File Coverage

lib/DBIx/EAV.pm
Criterion Covered Total %
statement 218 227 96.0
branch 76 114 66.6
condition 12 19 63.1
subroutine 29 29 100.0
pod 4 5 80.0
total 339 394 86.0


line stmt bran cond sub pod time code
1             package DBIx::EAV;
2              
3 10     10   2254571 use Moo;
  10         105299  
  10         46  
4 10     10   17523 use strictures 2;
  10         14521  
  10         527  
5 10     10   2152 use DBI;
  10         18  
  10         348  
6 10     10   6908 use Lingua::EN::Inflect ();
  10         271961  
  10         546  
7 10     10   100 use Data::Dumper;
  10         17  
  10         650  
8 10     10   59 use Digest::MD5 qw/ md5_hex /;
  10         16  
  10         507  
9 10     10   4346 use DBIx::EAV::EntityType;
  10         31  
  10         343  
10 10     10   4470 use DBIx::EAV::Entity;
  10         25  
  10         341  
11 10     10   3967 use DBIx::EAV::ResultSet;
  10         228  
  10         345  
12 10     10   4554 use DBIx::EAV::Schema;
  10         32  
  10         393  
13 10     10   64 use Carp qw' croak confess ';
  10         20  
  10         582  
14 10     10   62 use Scalar::Util 'blessed';
  10         17  
  10         561  
15 10     10   4485 use Class::Load qw' try_load_class ';
  10         114328  
  10         650  
16 10     10   4354 use namespace::clean;
  10         53042  
  10         69  
17              
18             our $VERSION = "0.11";
19              
20             # required
21             has 'dbh', is => 'ro', required => 1;
22              
23             # options
24             has 'default_attribute_type', is => 'ro', default => 'varchar';
25             has 'schema_config', is => 'ro', default => sub { {} };
26             has 'entity_namespaces', is => 'ro', default => sub { [] };
27             has 'resultset_namespaces', is => 'ro', default => sub { [] };
28              
29             # internal
30             has 'schema', is => 'ro', lazy => 1, builder => 1, init_arg => undef, handles => [qw/ table dbh_do /];
31             has '_type_declarations', is => 'ro', default => sub { {} };
32             has '_types', is => 'ro', default => sub { {} };
33             has '_types_by_id', is => 'ro', default => sub { {} };
34              
35             # group schema_config params
36             around BUILDARGS => sub {
37             my ( $orig, $class, @args ) = @_;
38             my $params = @args == 1 && ref $args[0] ? $args[0] : { @args };
39             my $schema_config = delete $params->{schema_config} || {};
40              
41             my @schema_params = grep { exists $params->{$_} } qw/
42             tenant_id data_types database_cascade_delete static_attributes
43             table_prefix id_type default_attribute_type enable_multi_tenancy
44             /;
45              
46             @{$schema_config}{@schema_params} = delete @{$params}{@schema_params};
47              
48             $class->$orig(%$params, schema_config => $schema_config);
49             };
50              
51              
52             sub _build_schema {
53 11     11   449 my $self = shift;
54 11         36 DBIx::EAV::Schema->new(%{$self->schema_config}, dbh => $self->dbh);
  11         232  
55             }
56              
57             sub connect {
58 1     1 1 90 my ($class, $dsn, $user, $pass, $attrs, $constructor_params) = @_;
59              
60 1 50       7 croak 'Missing $dsn argument for connect()' unless $dsn;
61              
62 1 50       4 croak "connect() must be called as a class method."
63             if ref $class;
64              
65 1   50     6 $constructor_params //= {};
66              
67 1 50       14 $constructor_params->{dbh} = DBI->connect($dsn, $user, $pass, $attrs)
68             or die $DBI::errstr;
69              
70 1         8840 $class->new($constructor_params);
71             }
72              
73             sub type {
74 138     138 1 8037 my ($self, $name) = @_;
75 138 50       340 confess 'usage: eav->type($name)' unless $name;
76              
77             return $self->_types->{$name}
78 138 100       706 if exists $self->_types->{$name};
79              
80 48         179 my $type = $self->_load_or_register_type('name', $name);
81              
82 47 100       1541 confess "EntityType '$name' does not exist."
83             unless $type;
84              
85 45         118 $type;
86             }
87              
88             sub type_by_id {
89 79     79 0 176 my ($self, $value) = @_;
90              
91             return $self->_types_by_id->{$value}
92 79 50       388 if exists $self->_types_by_id->{$value};
93              
94 0 0       0 $self->_load_or_register_type('id', $value)
95             or confess "EntityType 'id=$value' does not exist.";
96             }
97              
98             sub declare_entities {
99 14     14 1 129822 my ($self, $schema) = @_;
100 14         90 my $declarations = $self->_type_declarations;
101              
102 14         45 local $Data::Dumper::Indent = 0;
103 14         40 local $Data::Dumper::Sortkeys = 1;
104 14         43 local $Data::Dumper::Maxdepth = 10;
105              
106 14         94 for my $name (sort keys %$schema) {
107              
108             # generate signature
109 49         175 my $entity_schema = $self->_normalize_entity_schema($name, $schema->{$name});
110 49         186 my $signature = md5_hex Dumper($entity_schema);
111              
112             # not declared yet
113 49 100       5157 if (!$declarations->{$name}) {
114 41         140 $declarations->{$name} = {
115             signature => $signature,
116             schema => $entity_schema
117             };
118 41         115 next;
119             }
120             else {
121              
122             # same schema, do nothing
123 8 100       37 next if $declarations->{$name}{signature} eq $signature;
124              
125             # its different, replace declaration and invalidate insalled type
126 1         30 printf STDERR "# %s declaration changed from %s to %s\n", $name, $declarations->{$name}{signature}, $signature;
127 1         16 $declarations->{$name} = {
128             signature => $signature,
129             schema => $entity_schema
130             };
131              
132 1         8 my $type_id = $self->_types->{$name}->id;
133 1         3 delete $self->_types->{$name};
134 1         6 delete $self->_types_by_id->{$type_id};
135             }
136             }
137             }
138              
139             sub _load_or_register_type {
140 48     48   113 my ($self, $field, $value) = @_;
141 48         136 my $declarations = $self->_type_declarations;
142              
143             # find registered type
144 48 100       1132 if (my $type_row = $self->table('entity_types')->select_one({ $field => $value })) {
145              
146             # find custom class to update type declaration
147 4 50       17 if (my $custom_entity_class = $self->_resolve_entity_class($type_row->{name})) {
148 0         0 $self->declare_entities({ $value => $custom_entity_class->type_definition });
149             }
150              
151             # update type registration if changed
152             my $declaration = $declarations->{$type_row->{name}}
153 4 50       10 or die "Found registered but not declared entity type '$type_row->{name}'";
154              
155 4         6 my $type;
156             # declaration didnt change, load from db
157 4 100       10 if ($declaration->{signature} eq $type_row->{signature}) {
158              
159             # printf STDERR "# loaded $type_row->{name} signature %s.\n", $type_row->{signature};
160 2         15 $type = DBIx::EAV::EntityType->load({ %$type_row, core => $self});
161             }
162             # update definition
163             else {
164             # printf STDERR "# loaded $type_row->{name} signature changed from %s to %s.\n", $type_row->{signature}, $declaration->{signature};
165 2         7 $self->_update_type_definition($type_row, $declaration->{schema});
166 2         51 $type = DBIx::EAV::EntityType->new({ %$type_row, core => $self});
167             }
168              
169             # install type and return
170 4         72 $self->_types->{$type->name} = $type;
171 4         13 $self->_types_by_id->{$type->id} = $type;
172 4         41 return $type;
173             }
174              
175             # not found, give up unless we have a name
176 44 50       517 return unless $field eq 'name';
177              
178             # find custom class to update type declaration
179 44 100       163 if (my $custom_entity_class = $self->_resolve_entity_class($value)) {
180 3         13 $self->declare_entities({ $value => $custom_entity_class->type_definition });
181             }
182              
183             # declaration not found
184 43 100       146 return unless $declarations->{$value};
185              
186             # register new type
187 41         178 $self->_register_entity_type($value);
188             }
189              
190             sub _resolve_entity_class {
191 130     130   326 my ($self, $name) = @_;
192              
193 130         210 foreach my $ns (@{ $self->entity_namespaces }) {
  130         434  
194              
195 5         15 my $entity_class = join '::', $ns, $name;
196 5         21 my ($is_loaded, $error) = try_load_class $entity_class;
197              
198 5 100       669 return $entity_class if $is_loaded;
199              
200             # rethrow compilation errors
201 1 50       28 die $error if $error =~ /^Can't locate .* in \@INC/;
202             }
203              
204 125         2463 return;
205             }
206              
207             sub _resolve_resultset_class {
208 90     90   212 my ($self, $name) = @_;
209              
210 90         145 foreach my $ns (@{ $self->resultset_namespaces }) {
  90         294  
211              
212 2         7 my $class = join '::', $ns, $name;
213 2         8 my ($is_loaded, $error) = try_load_class $class;
214              
215 2 50       1058 return $class if $is_loaded;
216              
217             # rethrow compilation errors
218 0         0 die $class;
219             }
220              
221 88         405 return;
222             }
223              
224             sub resultset {
225 90     90 1 13868 my ($self, $name) = @_;
226 90         181 my $type;
227              
228 90 100       357 if (blessed $name) {
229 23 50       174 confess "invalid argument" unless $name->isa('DBIx::EAV::EntityType');
230 23         45 $type = $name;
231             }
232             else {
233 67         214 $type = $self->type($name);
234             }
235              
236 90   100     356 my $rs_class = $self->_resolve_resultset_class($type->name)
237             || 'DBIx::EAV::ResultSet';
238              
239 90         2566 $rs_class->new({
240             eav => $self,
241             type => $type,
242             });
243             }
244              
245             sub _register_entity_type {
246 41     41   136 my ($self, $name) = @_;
247              
248             # error: undeclared type
249 41 50       162 my $declaration = $self->_type_declarations->{$name}
250             or die "_register_entity_type() error: No type declaration for '$name'";
251              
252             # error: already registered
253 41         959 my $types_table = $self->table('entity_types');
254 41 50       223 if (my $type = $types_table->select_one({ name => $name })) {
255 0         0 die "Type '$type->{name}' is already registered!'";
256             }
257              
258             # isnert new entity type
259 41         554 my $id = $types_table->insert({ name => $name, signature => $declaration->{signature} });
260 41         235 my $type = $types_table->select_one({ id => $id });
261 41 50       446 die "Error inserting entity type '$name'!" unless $type;
262              
263             # insert type definition (parent, attributes, relationships)
264 41         222 $self->_update_type_definition($type, $declaration->{schema});
265              
266             # install and return
267             $self->_types->{$name} =
268 41         989 $self->_types_by_id->{$type->{id}} = DBIx::EAV::EntityType->new(%$type, core => $self);
269             }
270              
271              
272             sub _update_type_definition {
273 43     43   103 my ($self, $type, $spec) = @_;
274              
275             # parent type first
276 43         134 my $parent_type = $self->_update_type_inheritance($type, $spec);
277 43 100       105 $type->{parent} = $parent_type if $parent_type;
278              
279             # update or create attributes
280 43         188 $self->_update_type_attributes($type, $spec);
281              
282             # update or create relationships
283 43         95 foreach my $reltype (qw/ has_one has_many many_to_many /) {
284              
285 129 100       350 next unless defined $spec->{$reltype};
286              
287             $spec->{$reltype} = [$spec->{$reltype}]
288 38 50       126 unless ref $spec->{$reltype} eq 'ARRAY';
289              
290 38         60 foreach my $rel (@{$spec->{$reltype}}) {
  38         91  
291             # $entity_type->register_relationship($reltype, $rel);
292 38         150 $self->_register_type_relationship($type, $reltype, $rel);
293             }
294             }
295              
296             }
297              
298             sub _update_type_inheritance {
299 43     43   86 my ($self, $type, $spec) = @_;
300              
301 43         1045 my $hierarchy_table = $self->table('type_hierarchy');
302 43         203 my $inheritance_row = $hierarchy_table->select_one({ child_type_id => $type->{id} });
303 43         349 my $parent_type;
304              
305 43 100       159 if ($spec->{extends}) {
306              
307             die "Unknown type '$spec->{extends}' specified in 'extents' option for type '$type->{name}'."
308 5 50       20 unless $parent_type = $self->type($spec->{extends});
309              
310             # update parent link
311 5 50 33     23 if ($inheritance_row && $inheritance_row->{parent_type_id} ne $parent_type->id) {
    50          
312              
313 0 0       0 $hierarchy_table->update({ parent_type_id => $parent_type->id }, $inheritance_row)
314             or die "Error updating to inheritance table. ( for '$type->{name}' extends '$spec->{extends}')";
315             }
316             # insert parent link
317             elsif(!$inheritance_row) {
318              
319 5 50       25 $hierarchy_table->insert({ child_type_id => $type->{id}, parent_type_id => $parent_type->id })
320             or die "Error inserting to inheritance table. ( for '$type->{name}' extends '$spec->{extends}')";
321             }
322              
323 5         19 $type->{parent} = $parent_type;
324             }
325             else {
326             # remove parent link
327 38 50       196 if ($inheritance_row) {
328 0 0       0 $hierarchy_table->delete($inheritance_row)
329             or die "Error deleting from inheritance table. (to remove '$type->{name}' parent link)";
330             }
331             }
332              
333 43         181 $parent_type;
334             }
335              
336             sub _update_type_attributes {
337 43     43   89 my ($self, $type, $spec) = @_;
338              
339 43         1029 my $attributes = $self->table('attributes');
340 43         148 my %static_attributes = map { $_ => {name => $_, is_static => 1} } @{$self->table('entities')->columns};
  128         489  
  43         751  
341 43         116 $type->{attributes} = {};
342              
343 43 100       148 my %inherited_attributes = $type->{parent} ? map { $_->{name} => $_ } $type->{parent} ->attributes( no_static => 1 ) : ();
  21         51  
344              
345 43         62 foreach my $attr_spec (@{$spec->{attributes}}) {
  43         159  
346              
347             printf STDERR "[warn] entity '%s' is overriding inherited attribute '%s'", $type->{name}, $attr_spec->{name}
348 122 50       295 if $inherited_attributes{$attr_spec->{name}};
349              
350             my $attr = $attributes->select_one({
351             entity_type_id => $type->{id},
352             name => $attr_spec->{name}
353 122         487 });
354              
355 122 100       1206 if (defined $attr) {
356             # TODO update attribute definition
357             }
358             else {
359 113         231 delete $attr_spec->{id}; # safety
360              
361 113         487 my %data = %$attr_spec;
362              
363 113         246 $data{entity_type_id} = $type->{id};
364 113   66     371 $data{data_type} = delete($data{type}) || $self->default_attribute_type;
365              
366             die sprintf("Attribute '%s' has unknown data type '%s'.", $data{name}, $data{data_type})
367 113 50       2766 unless $self->schema->has_data_type($data{data_type});
368              
369 113         373 $attributes->insert(\%data);
370 113         384 $attr = $attributes->select_one(\%data);
371 113 50       1259 die "Error inserting attribute '$attr_spec->{name}'!" unless $attr;
372             }
373              
374 122         565 $type->{attributes}{$attr->{name}} = $attr;
375             }
376             }
377              
378             sub _register_type_relationship {
379 38     38   128 my ($self, $type, $reltype, $params) = @_;
380              
381             die sprintf("Error: invalid %s relationship for entity '%s': missing 'entity' parameter.", $reltype, $type->{name})
382 38 50       114 unless $params->{entity};
383              
384 38         136 my $other_entity = $self->type($params->{entity});
385              
386 38 100 66     367 $params->{name} ||= $reltype =~ /_many$/ ? lc Lingua::EN::Inflect::PL($other_entity->name)
387             : lc $other_entity->name;
388              
389             $params->{incoming_name} ||= $reltype eq 'many_to_many' ? lc Lingua::EN::Inflect::PL($type->{name})
390 38 100 66     36646 : lc $type->{name};
391              
392             my %rel = (
393             left_entity_type_id => $type->{id},
394             right_entity_type_id => $other_entity->id,
395             name => $params->{name},
396             incoming_name => $params->{incoming_name},
397 38         3437 "is_$reltype" => 1
398             );
399              
400             # update or insert
401 38         864 my $relationships_table = $self->table('relationships');
402             my $existing_rel = $relationships_table->select_one({
403             left_entity_type_id => $type->{id},
404             name => $rel{name},
405 38         218 });
406              
407 38 100       429 if ($existing_rel) {
408              
409 4         10 $rel{id} = $existing_rel->{id};
410              
411             # update
412 0         0 my %changed_cols = map { $_ => $rel{$_} }
413 4         12 grep { $rel{$_} ne $existing_rel->{$_} }
  24         43  
414             keys %rel;
415              
416             $relationships_table->update(\%changed_cols, { id => $rel{id} })
417 4 50       13 if keys %changed_cols > 0;
418             }
419             else {
420 34         127 my $id = $relationships_table->insert(\%rel);
421             die sprintf("Database error while registering '%s -> %s' relationship.", $type->{name}, $rel{name})
422 34 50       117 unless $id;
423              
424 34         85 $rel{id} = $id;
425             }
426              
427             # this type side
428 38         138 $type->{relationships}->{$rel{name}} = \%rel;
429              
430             # install their side
431             $other_entity->_relationships->{$rel{incoming_name}} = {
432             %rel,
433             is_right_entity => 1,
434             name => $rel{incoming_name},
435             incoming_name => $rel{name},
436 38         434 };
437             }
438              
439             sub _normalize_entity_schema {
440 49     49   102 my ($self, $entity_name, $schema) = @_;
441              
442             # validate, normalize and copy data structures
443 49         66 my %normalized;
444              
445             # scalar keys
446 49         78 for (qw/ extends /) {
447             $normalized{$_} = $schema->{$_}
448 49 100       154 if exists $schema->{$_};
449             }
450              
451             # attributes
452 49         70 my %static_attributes = map { $_ => {name => $_, is_static => 1} } @{$self->table('entities')->columns};
  140         448  
  49         1092  
453 49         89 foreach my $attr_spec (@{$schema->{attributes}}) {
  49         123  
454              
455             # expand string to name/type
456 137 100       236 unless (ref $attr_spec) {
457 46         131 my ($name, $type) = split ':', $attr_spec;
458 46   66     273 $attr_spec = {
459             name => $name,
460             type => $type || $self->default_attribute_type
461             };
462             }
463              
464             die sprintf("Error normalizing attribute '%s' for entity '%s': can't use names of static attributes (real table columns).", $attr_spec->{name}, $entity_name)
465 137 50       292 if exists $static_attributes{$attr_spec->{name}};
466              
467 137         129 push @{$normalized{attributes}}, { %$attr_spec };
  137         551  
468             }
469              
470             # relationships
471 49         96 for my $reltype (qw/ has_one has_many many_to_many /) {
472              
473 147 100       273 next unless $schema->{$reltype};
474              
475 42         79 my $rels = $schema->{$reltype};
476 42 100       99 if (my $reftype = ref $rels) {
477 21 50       68 die "Error: invalid '$reltype' config for '$entity_name'" if $reftype ne 'ARRAY';
478             } else {
479 21         43 $rels = [$rels]
480             }
481              
482 42         74 foreach my $params (@$rels) {
483              
484 42         51 my %rel;
485 42         75 my $reftype = ref $params;
486             # scalar: entity
487 42 100       109 if (!$reftype) {
    50          
    0          
488 32         90 %rel = ( entity => $params )
489             }
490             elsif ($reftype eq 'ARRAY') {
491              
492 10         68 %rel = (
493             name => $params->[0],
494             entity => $params->[1],
495             incoming_name => $params->[2],
496             );
497             }
498             elsif ($reftype eq 'HAS') {
499 0         0 %rel = %$params;
500             }
501             else {
502 0         0 die "Error: invalid '$reltype' config for '$entity_name'.";
503             }
504              
505             die sprintf("Error: invalid %s relationship for entity '%s': missing 'entity' parameter.", $reltype, $entity_name)
506 42 50       93 unless $rel{entity};
507              
508             # push
509 42         59 push @{$normalized{$reltype}}, \%rel;
  42         142  
510             }
511              
512             }
513              
514 49         165 \%normalized;
515             }
516              
517             1;
518              
519             __END__