File Coverage

lib/DBIx/EAV/Entity.pm
Criterion Covered Total %
statement 188 203 92.6
branch 73 106 68.8
condition 37 64 57.8
subroutine 23 25 92.0
pod 11 13 84.6
total 332 411 80.7


line stmt bran cond sub pod time code
1             package DBIx::EAV::Entity;
2              
3 10     10   36 use Moo;
  10         9  
  10         46  
4 10     10   1827 use strictures 2;
  10         50  
  10         426  
5 10     10   1618 use Scalar::Util qw/ blessed /;
  10         11  
  10         418  
6 10     10   32 use Data::Dumper;
  10         10  
  10         351  
7 10     10   34 use Carp 'croak';
  10         11  
  10         18580  
8              
9             has 'eav', is => 'ro', required => 1;
10             has 'type', is => 'ro', required => 1, handles => [qw/ is_type /];
11             has 'raw', is => 'ro', default => sub { {} };
12             has '_modified', is => 'ro', default => sub { {} };
13             has '_modified_related', is => 'ro', default => sub { {} };
14              
15              
16              
17             sub in_storage {
18 189     189 1 5439 my $self = shift;
19 189 100       780 exists $self->raw->{id} && defined $self->raw->{id};
20             }
21              
22             sub id {
23 890     890 1 2181 my $self = shift;
24 890 50       1605 return unless exists $self->raw->{id};
25 890         3540 $self->raw->{id};
26             }
27              
28              
29              
30             sub get {
31 107     107 1 595 my $self = shift;
32 107         97 my $name = shift;
33 107         141 my $type = $self->type;
34              
35 107 100       203 return $self->raw->{$name}
36             if $type->has_attribute($name);
37              
38 20 50       42 if ($type->has_relationship($name)) {
39 20         32 my $rel = $type->relationship($name);
40 20         42 my $rs = $self->_get_related($name, @_);
41             # return an Entity for has_one and belongs_to; return Cursor otherwise
42             return $rs->next if
43 20 100 66     110 $rel->{is_has_one} || ($rel->{is_has_many} && $rel->{is_right_entity});
      66        
44              
45             # *_many rel, return cursor or array of entities
46 16 50       60 return wantarray ? $rs->all : $rs;
47             }
48              
49 0         0 die sprintf "get() error: '%s' is not a valid attribute/relationship for '%s'", $name, $self->type->name;
50             }
51              
52             sub _get_related {
53 41     41   50 my ($self, $relname, $query, $options) = @_;
54 41   100     134 $query //= {};
55 41         91 my $rel = $self->type->relationship($relname);
56 41         73 $query->{$rel->{incoming_name}} = $self;
57 41         121 $self->eav->resultset($rel->{entity})->search($query, $options);
58             }
59              
60              
61             sub load_attributes {
62 112     112 1 139 my ($self, @attrs) = @_;
63              
64 112 50       164 die "Can't load_attributes(): this entity has no id!"
65             unless defined $self->id;
66              
67 112         136 my $eav = $self->eav;
68 112         116 my $type = $self->type;
69              
70 112 50       372 @attrs = $type->attributes( no_static => 1, names => 1 )
71             if @attrs == 0;
72              
73             # build sql query: one aliases subselect for each attribute
74             my $sql_query = 'SELECT ' . join(', ', map {
75              
76 112         139 my $attr_spec = $type->attribute($_);
  436         739  
77 436         6997 my $value_table = $eav->table('value_'. $attr_spec->{data_type} );
78             sprintf "(SELECT value FROM %s WHERE entity_id = %d AND attribute_id = %d) AS %s",
79             $value_table->name,
80             $self->id,
81             $attr_spec->{id},
82 436         877 $_;
83              
84             } @attrs);
85              
86             # fetch data
87 112         1626 my ($rv, $sth) = $eav->dbh_do($sql_query);
88 112         1691 my $data = $sth->fetchrow_hashref;
89              
90 112 50       376 die "load_attributes() failed! No data returned from database!"
91             unless ref $data eq 'HASH';
92              
93 112         182 my $raw = $self->raw;
94 112         114 my $total = 0;
95              
96             # adopt data
97 112         352 for (keys %$data) {
98 436         444 $raw->{$_} = $data->{$_};
99 436         358 $total++;
100             }
101              
102             # return the number os attrs loaded
103 112         1356 $total;
104             }
105              
106             sub update {
107 0     0 1 0 my $self = shift;
108 0         0 $self->set(@_)->save;
109             }
110              
111             sub set {
112 98     98 1 2069 my $self = shift;
113 98         156 my $numargs = scalar(@_);
114              
115 98 50 33     402 die 'Call set(\%data) or set($attr, $value)'
116             if 1 > $numargs || $numargs > 2;
117              
118 98 100       252 if ($numargs == 2) {
    50          
119 2         8 $self->_set(@_);
120             }
121             elsif ($numargs == 1) {
122 96 50       255 die "You must pass a hashref set()" unless ref $_[0] eq 'HASH';
123 96         92 while (my ($k, $v) = each %{$_[0]}) {
  247         690  
124 151         237 $self->_set($k, $v);
125             }
126             }
127              
128 98         136 $self;
129             }
130              
131             sub _set {
132 153     153   158 my ($self, $attr_name, $value) = @_;
133 153         209 my $type = $self->type;
134              
135 153 100       319 if ($type->has_relationship($attr_name)) {
136 16         31 return $self->_set_related($attr_name, $value);
137             }
138              
139 137         281 my $attr = $self->type->attribute($attr_name);
140              
141 137 50       320 die "Sorry, you can't set the 'id' attribute."
142             if $attr_name eq 'id';
143              
144             # same value
145             return if defined $value &&
146             exists $self->raw->{$attr_name} &&
147             defined $self->raw->{$attr_name} &&
148 137 50 66     762 $value eq $self->raw->{$attr_name};
      100        
      66        
149              
150             # remember original value
151             $self->_modified->{$attr_name} = $self->raw->{$attr_name}
152 137 100       428 unless exists $self->_modified->{$attr_name};
153              
154              
155             # set
156             # TODO use type-specific deflator
157 137         300 $self->raw->{$attr_name} = $value;
158             }
159              
160             sub _set_related {
161 16     16   19 my ($self, $relname, $data) = @_;
162 16         25 my $type = $self->type;
163 16         36 my $rel = $type->relationship($relname);
164              
165 16 0 66     74 die "You can only pass related data in the form of a hashref, blessed Entity object, or an arrayref of it."
      0        
      33        
166             unless ref $data eq 'HASH' || ref $data eq 'ARRAY' || (blessed $data && $data->isa('DBIx::EAV::Entity'));
167              
168             die "You can't pass an arrayref for the '$rel->{name}' relationship."
169 16 50 33     78 if ref $data eq 'ARRAY' && ( $rel->{is_has_one} || ($rel->{is_has_many} && $rel->{is_right_entity}) );
      66        
170              
171 16         29 $self->raw->{$relname} = $data;
172 16         42 $self->_modified_related->{$relname} = 1;
173             }
174              
175              
176             sub save {
177 95     95 1 108 my $self = shift;
178 95         112 my $type = $self->type;
179 95         1617 my $entities_table = $self->eav->table('entities');
180 95         173 my $is_new_entity = not $self->in_storage;
181 95         107 my $raw = $self->raw;
182              
183             # modified static attrs
184 3         17 my %modified_static_attributes = map { $_ => $self->raw->{$_} }
185 133         409 grep { $type->has_static_attribute($_) }
186 95         84 keys %{$self->_modified};
  95         209  
187              
188             # insert if its new entity
189 95 100       611 if ($is_new_entity) {
190              
191             # TODO insert default values
192              
193 91         346 my $id = $entities_table->insert({
194             %modified_static_attributes,
195             entity_type_id => $type->id,
196             });
197              
198 91 50       218 die "Invalid ID returned ($id) while inserting new entity."
199             unless $id > 0;
200              
201 91         301 my $static_attributes = $entities_table->select_one({ id => $id });
202              
203             die "Error: could not fetch the entity row I've just inserted!"
204 91 50       953 unless $static_attributes->{id} == $id;
205              
206             $raw->{$_} = $static_attributes->{$_}
207 91         429 for keys %$static_attributes;
208              
209             # undirty those attrs
210 91         138 delete $self->_modified->{$_} for keys %modified_static_attributes;
211 91         181 %modified_static_attributes = ();
212             }
213              
214             # upsert attributes
215 95         87 my $modified_count = 0;
216              
217 95         90 while (my ($attr_name, $old_value) = each %{$self->_modified}) {
  227         846  
218              
219 132         113 $modified_count++;
220 132         140 my $value = $raw->{$attr_name};
221 132         385 my $attr_spec = $self->type->attribute($attr_name);
222              
223             # save static attrs later
224 132 100       255 if ($attr_spec->{is_static}) {
225 2         3 $modified_static_attributes{$attr_name} = $value;
226 2         4 next;
227             }
228              
229 130         2770 my $values_table = $self->eav->table('value_'.$attr_spec->{data_type});
230              
231             my %attr_criteria = (
232             entity_id => $self->id,
233             attribute_id => $attr_spec->{id}
234 130         338 );
235              
236             # undefined value, delete attribute row
237 130 100       294 if (not defined $value) {
    100          
238 1         6 $values_table->delete(\%attr_criteria);
239             }
240              
241             # update or insert value
242             elsif (defined $old_value) {
243 1         6 $values_table->update({ value => $value }, \%attr_criteria);
244             }
245             else {
246 128         515 $values_table->insert({
247             %attr_criteria,
248             value => $value
249             });
250             }
251             }
252              
253             # upset related
254 95         91 foreach my $relname (keys %{$self->_modified_related}) {
  95         265  
255 16         50 $self->_save_related($relname, $self->raw->{$relname});
256             }
257              
258              
259             # update static attributes
260 95 50       186 if ($modified_count > 0) {
261              
262 95 100       169 $entities_table->update(\%modified_static_attributes, { id => $self->id })
263             if keys(%modified_static_attributes) > 0;
264             }
265              
266             # undirty
267 95         87 %{$self->_modified} = ();
  95         177  
268              
269 95         279 $self;
270             }
271              
272             sub _save_related {
273 18     18   27 my ($self, $relname, $data, $options) = @_;
274 18   100     71 $options //= {};
275              
276 18         54 my $rel = $self->type->relationship($relname);
277 18         66 my $related_type = $self->eav->type($rel->{entity});
278 18 50       42 my ($our_side, $their_side) = $rel->{is_right_entity} ? qw/ right left / : qw/ left right /;
279              
280             # delete any old links
281 18         362 my $relationship_table = $self->eav->table('entity_relationships');
282             $relationship_table->delete({
283             relationship_id => $rel->{id},
284             $our_side."_entity_id" => $self->id
285 18 100       86 }) unless $options->{keep_current_links};
286              
287             # link new objects
288 18 100       71 foreach my $entity (ref $data eq 'ARRAY' ? @$data : ($data)) {
289              
290             # if is a blessed object, check its a entity from the correct type
291 51 100       165 if (blessed $entity) {
    50          
292              
293 22 50       74 die "Can't save data for relationship '$relname': unknown data type: ". ref $entity
294             unless $entity->isa('DBIx::EAV::Entity');
295              
296 22 50       67 die sprintf("relationship '%s' requires '%s' objects, not '%s'", $relname, $related_type->name, $entity->type->name)
297             unless $entity->type->id == $related_type->id;
298              
299 22 50       33 die "Can't save data for relationship '$relname': related entity is not in_storage."
300             unless $entity->in_storage;
301              
302             # remove any links to it
303             $relationship_table->delete({
304             relationship_id => $rel->{id},
305             $their_side."_entity_id" => $entity->id
306              
307 22 100       46 }) unless $rel->{is_many_to_many};
308              
309             }
310             elsif (ref $entity eq 'HASH') {
311              
312             # insert new entity
313 29         116 $entity = $self->eav->resultset($related_type->name)->insert($entity);
314             }
315             else {
316 0         0 die "Can't save data for relationship '$relname': unknown data type: ". ref $entity;
317             }
318              
319             # create link
320             $relationship_table->insert({
321             relationship_id => $rel->{id},
322 51 50       175 $our_side."_entity_id" => $self->id,
323             $their_side."_entity_id" => $entity->id
324             }) or die "Error creating link for relationship '$relname'";
325             }
326             }
327              
328             sub add_related {
329 3     3 1 20 my ($self, $relname, $data) = @_;
330 3         13 my $rel = $self->type->relationship($relname);
331             die "Can't call add_related() for relationship '$rel->{name}'"
332 3 100 66     28 if $rel->{is_has_one} || ($rel->{is_has_many} && $rel->{is_right_entity});
      66        
333              
334 2         6 $self->_save_related($relname, $data, { keep_current_links => 1 });
335             }
336              
337              
338             sub remove_related {
339 3     3 1 374 my ($self, $relname, $data) = @_;
340 3         12 my $rel = $self->type->relationship($relname);
341              
342             die "Can't call add_related() for relationship '$rel->{name}'"
343 3 100 66     23 if $rel->{is_has_one} || ($rel->{is_has_many} && $rel->{is_right_entity});
      66        
344              
345 2         40 my $relationships_table = $self->eav->table('entity_relationships');
346 2 50       8 my ($our_side, $their_side) = $rel->{is_right_entity} ? qw/ right left / : qw/ left right /;
347              
348 2 50       7 $data = [$data] unless ref $data eq 'ARRAY';
349              
350 2         4 foreach my $entity (@$data) {
351              
352             die "remove_related() error: give me an instance of '$rel->{entity}' or an arrayref of it."
353 2 50 33     24 unless blessed $entity && $entity->isa('DBIx::EAV::Entity') && $entity->type->name eq $rel->{entity};
      33        
354              
355             $relationships_table->delete({
356             relationship_id => $rel->{id},
357 2         8 $our_side ."_entity_id" => $self->id,
358             $their_side."_entity_id" => $entity->id
359             });
360             }
361             }
362              
363              
364             sub discard_changes {
365 0     0 1 0 my $self = shift;
366              
367 0         0 while (my ($k, $v) = each %{$self->_modified}) {
  0         0  
368 0         0 $self->raw->{$k} = $v;
369 0         0 delete $self->raw->{$k};
370             }
371              
372 0         0 $self;
373             }
374              
375              
376             sub delete {
377 24     24 1 35 my $self = shift;
378 24 50       44 die "Can't delete coz I'm not in storage!"
379             unless $self->in_storage;
380              
381 24         47 my $eav = $self->eav;
382 24         37 my $type = $self->type;
383              
384             # cascade delete child entities
385 24         60 foreach my $rel ($type->relationships) {
386              
387             next if $rel->{is_right_entity}
388             || $rel->{is_many_to_many}
389 53 100 66     189 || (exists $rel->{cascade_delete} && $rel->{cascade_delete} == 0);
      33        
      66        
390              
391 21         38 my $rs = $self->_get_related($rel->{name});
392 21         69 while (my $related_entity = $rs->next) {
393 6         21 $related_entity->delete;
394             }
395             }
396              
397 24 50       516 unless ($eav->schema->database_cascade_delete) {
398              
399             # delete relationship links
400 0         0 $eav->table('entity_relationships')->delete([
401             { left_entity_id => $self->id },
402             { right_entity_id => $self->id }
403             ]);
404              
405             # delete attributes
406 0         0 my %data_types = map { $_->{data_type} => 1 }
  0         0  
407             $type->attributes( no_static => 1 );
408              
409 0         0 foreach my $data_type (keys %data_types) {
410 0         0 $eav->table('value_'.$data_type)->delete({ entity_id => $self->id });
411             }
412             }
413              
414             # delete entity
415 24         521 my $entities_table = $self->eav->table('entities');
416 24         50 my $rv = $entities_table->delete({ id => $self->id });
417 24         83 delete $self->raw->{id}; # not in_storage
418 24         64 $rv;
419             }
420              
421              
422             ## ##
423             ## Class Methods ##
424             ## ##
425              
426             sub is_custom_class {
427 20     20 0 4085 my $class = shift;
428 20 50       26 croak "is_custom_class() is a Class method." if ref $class;
429 20         46 $class ne __PACKAGE__;
430             }
431              
432             sub type_definition {
433 11     11 0 164 my $class = shift;
434              
435 11 50       17 croak "type_definition() is a Class method." if ref $class;
436 11 50       14 croak "type_definition() must be called on DBIx::EAV::Entity subclasses."
437             unless $class->is_custom_class;
438              
439 10     10   50 no strict 'refs';
  10         9  
  10         1378  
440 11 100       10 unless (defined *{"${class}::__TYPE_DEFINITION__"}) {
  11         36  
441              
442 3         2 my %definition;
443             # detect parent entity
444 3         4 my $parent_class = ${"${class}::ISA"}[0];
  3         8  
445 3 100       12 ($definition{extends}) = $parent_class =~ /::(\w+)$/
446             if $parent_class ne __PACKAGE__;
447              
448 3         5 *{"${class}::__TYPE_DEFINITION__"} = \%definition;
  3         9  
449             }
450              
451              
452 11         6 \%{"${class}::__TYPE_DEFINITION__"};
  11         51  
453             }
454              
455             # install class methods for type definition
456             foreach my $stuff (qw/ attribute has_many has_one many_to_many /) {
457 10     10   38 no strict 'refs';
  10         11  
  10         1107  
458             *{$stuff} = sub {
459 7     7   8039 my ($class, $spec) = @_;
460              
461 7 50       16 croak "$stuff() is a Class method." if ref $class;
462 7 50       17 croak "$stuff() must be called on DBIx::EAV::Entity subclasses."
463             unless $class->is_custom_class;
464              
465 7 100       15 my $key = $stuff eq 'attribute' ? 'attributes' : $stuff;
466 7         4 push @{ $class->type_definition->{$key} }, $spec;
  7         15  
467             };
468             }
469              
470              
471              
472             1;
473              
474              
475             __END__