File Coverage

lib/DBIx/EAV/Entity.pm
Criterion Covered Total %
statement 191 206 92.7
branch 72 108 66.6
condition 34 64 53.1
subroutine 23 25 92.0
pod 11 13 84.6
total 331 416 79.5


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