File Coverage

lib/DBIx/EAV/Entity.pm
Criterion Covered Total %
statement 21 203 10.3
branch 0 106 0.0
condition 0 64 0.0
subroutine 7 25 28.0
pod 11 13 84.6
total 39 411 9.4


line stmt bran cond sub pod time code
1             package DBIx::EAV::Entity;
2              
3 10     10   35 use Moo;
  10         10  
  10         44  
4 10     10   1780 use strictures 2;
  10         43  
  10         319  
5 10     10   1430 use Scalar::Util qw/ blessed /;
  10         16  
  10         414  
6 10     10   35 use Data::Dumper;
  10         13  
  10         346  
7 10     10   36 use Carp 'croak';
  10         11  
  10         18991  
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 0     0 1   my $self = shift;
19 0 0         exists $self->raw->{id} && defined $self->raw->{id};
20             }
21              
22             sub id {
23 0     0 1   my $self = shift;
24 0 0         return unless exists $self->raw->{id};
25 0           $self->raw->{id};
26             }
27              
28              
29              
30             sub get {
31 0     0 1   my $self = shift;
32 0           my $name = shift;
33 0           my $type = $self->type;
34              
35 0 0         return $self->raw->{$name}
36             if $type->has_attribute($name);
37              
38 0 0         if ($type->has_relationship($name)) {
39 0           my $rel = $type->relationship($name);
40 0           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 0 0 0       $rel->{is_has_one} || ($rel->{is_has_many} && $rel->{is_right_entity});
      0        
44              
45             # *_many rel, return cursor or array of entities
46 0 0         return wantarray ? $rs->all : $rs;
47             }
48              
49 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 0     0     my ($self, $relname, $query, $options) = @_;
54 0   0       $query //= {};
55 0           my $rel = $self->type->relationship($relname);
56 0           $query->{$rel->{incoming_name}} = $self;
57 0           $self->eav->resultset($rel->{entity})->search($query, $options);
58             }
59              
60              
61             sub load_attributes {
62 0     0 1   my ($self, @attrs) = @_;
63              
64 0 0         die "Can't load_attributes(): this entity has no id!"
65             unless defined $self->id;
66              
67 0           my $eav = $self->eav;
68 0           my $type = $self->type;
69              
70 0 0         @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 0           my $attr_spec = $type->attribute($_);
  0            
77 0           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 0           $_;
83              
84             } @attrs);
85              
86             # fetch data
87 0           my ($rv, $sth) = $eav->dbh_do($sql_query);
88 0           my $data = $sth->fetchrow_hashref;
89              
90 0 0         die "load_attributes() failed! No data returned from database!"
91             unless ref $data eq 'HASH';
92              
93 0           my $raw = $self->raw;
94 0           my $total = 0;
95              
96             # adopt data
97 0           for (keys %$data) {
98 0           $raw->{$_} = $data->{$_};
99 0           $total++;
100             }
101              
102             # return the number os attrs loaded
103 0           $total;
104             }
105              
106             sub update {
107 0     0 1   my $self = shift;
108 0           $self->set(@_)->save;
109             }
110              
111             sub set {
112 0     0 1   my $self = shift;
113 0           my $numargs = scalar(@_);
114              
115 0 0 0       die 'Call set(\%data) or set($attr, $value)'
116             if 1 > $numargs || $numargs > 2;
117              
118 0 0         if ($numargs == 2) {
    0          
119 0           $self->_set(@_);
120             }
121             elsif ($numargs == 1) {
122 0 0         die "You must pass a hashref set()" unless ref $_[0] eq 'HASH';
123 0           while (my ($k, $v) = each %{$_[0]}) {
  0            
124 0           $self->_set($k, $v);
125             }
126             }
127              
128 0           $self;
129             }
130              
131             sub _set {
132 0     0     my ($self, $attr_name, $value) = @_;
133 0           my $type = $self->type;
134              
135 0 0         if ($type->has_relationship($attr_name)) {
136 0           return $self->_set_related($attr_name, $value);
137             }
138              
139 0           my $attr = $self->type->attribute($attr_name);
140              
141 0 0         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 0 0 0       $value eq $self->raw->{$attr_name};
      0        
      0        
149              
150             # remember original value
151             $self->_modified->{$attr_name} = $self->raw->{$attr_name}
152 0 0         unless exists $self->_modified->{$attr_name};
153              
154              
155             # set
156             # TODO use type-specific deflator
157 0           $self->raw->{$attr_name} = $value;
158             }
159              
160             sub _set_related {
161 0     0     my ($self, $relname, $data) = @_;
162 0           my $type = $self->type;
163 0           my $rel = $type->relationship($relname);
164              
165 0 0 0       die "You can only pass related data in the form of a hashref, blessed Entity object, or an arrayref of it."
      0        
      0        
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 0 0 0       if ref $data eq 'ARRAY' && ( $rel->{is_has_one} || ($rel->{is_has_many} && $rel->{is_right_entity}) );
      0        
170              
171 0           $self->raw->{$relname} = $data;
172 0           $self->_modified_related->{$relname} = 1;
173             }
174              
175              
176             sub save {
177 0     0 1   my $self = shift;
178 0           my $type = $self->type;
179 0           my $entities_table = $self->eav->table('entities');
180 0           my $is_new_entity = not $self->in_storage;
181 0           my $raw = $self->raw;
182              
183             # modified static attrs
184 0           my %modified_static_attributes = map { $_ => $self->raw->{$_} }
185 0           grep { $type->has_static_attribute($_) }
186 0           keys %{$self->_modified};
  0            
187              
188             # insert if its new entity
189 0 0         if ($is_new_entity) {
190              
191             # TODO insert default values
192              
193 0           my $id = $entities_table->insert({
194             %modified_static_attributes,
195             entity_type_id => $type->id,
196             });
197              
198 0 0         die "Invalid ID returned ($id) while inserting new entity."
199             unless $id > 0;
200              
201 0           my $static_attributes = $entities_table->select_one({ id => $id });
202              
203             die "Error: could not fetch the entity row I've just inserted!"
204 0 0         unless $static_attributes->{id} == $id;
205              
206             $raw->{$_} = $static_attributes->{$_}
207 0           for keys %$static_attributes;
208              
209             # undirty those attrs
210 0           delete $self->_modified->{$_} for keys %modified_static_attributes;
211 0           %modified_static_attributes = ();
212             }
213              
214             # upsert attributes
215 0           my $modified_count = 0;
216              
217 0           while (my ($attr_name, $old_value) = each %{$self->_modified}) {
  0            
218              
219 0           $modified_count++;
220 0           my $value = $raw->{$attr_name};
221 0           my $attr_spec = $self->type->attribute($attr_name);
222              
223             # save static attrs later
224 0 0         if ($attr_spec->{is_static}) {
225 0           $modified_static_attributes{$attr_name} = $value;
226 0           next;
227             }
228              
229 0           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 0           );
235              
236             # undefined value, delete attribute row
237 0 0         if (not defined $value) {
    0          
238 0           $values_table->delete(\%attr_criteria);
239             }
240              
241             # update or insert value
242             elsif (defined $old_value) {
243 0           $values_table->update({ value => $value }, \%attr_criteria);
244             }
245             else {
246 0           $values_table->insert({
247             %attr_criteria,
248             value => $value
249             });
250             }
251             }
252              
253             # upset related
254 0           foreach my $relname (keys %{$self->_modified_related}) {
  0            
255 0           $self->_save_related($relname, $self->raw->{$relname});
256             }
257              
258              
259             # update static attributes
260 0 0         if ($modified_count > 0) {
261              
262 0 0         $entities_table->update(\%modified_static_attributes, { id => $self->id })
263             if keys(%modified_static_attributes) > 0;
264             }
265              
266             # undirty
267 0           %{$self->_modified} = ();
  0            
268              
269 0           $self;
270             }
271              
272             sub _save_related {
273 0     0     my ($self, $relname, $data, $options) = @_;
274 0   0       $options //= {};
275              
276 0           my $rel = $self->type->relationship($relname);
277 0           my $related_type = $self->eav->type($rel->{entity});
278 0 0         my ($our_side, $their_side) = $rel->{is_right_entity} ? qw/ right left / : qw/ left right /;
279              
280             # delete any old links
281 0           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 0 0         }) unless $options->{keep_current_links};
286              
287             # link new objects
288 0 0         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 0 0         if (blessed $entity) {
    0          
292              
293 0 0         die "Can't save data for relationship '$relname': unknown data type: ". ref $entity
294             unless $entity->isa('DBIx::EAV::Entity');
295              
296 0 0         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 0 0         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 0 0         }) unless $rel->{is_many_to_many};
308              
309             }
310             elsif (ref $entity eq 'HASH') {
311              
312             # insert new entity
313 0           $entity = $self->eav->resultset($related_type->name)->insert($entity);
314             }
315             else {
316 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 0 0         $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 0     0 1   my ($self, $relname, $data) = @_;
330 0           my $rel = $self->type->relationship($relname);
331             die "Can't call add_related() for relationship '$rel->{name}'"
332 0 0 0       if $rel->{is_has_one} || ($rel->{is_has_many} && $rel->{is_right_entity});
      0        
333              
334 0           $self->_save_related($relname, $data, { keep_current_links => 1 });
335             }
336              
337              
338             sub remove_related {
339 0     0 1   my ($self, $relname, $data) = @_;
340 0           my $rel = $self->type->relationship($relname);
341              
342             die "Can't call add_related() for relationship '$rel->{name}'"
343 0 0 0       if $rel->{is_has_one} || ($rel->{is_has_many} && $rel->{is_right_entity});
      0        
344              
345 0           my $relationships_table = $self->eav->table('entity_relationships');
346 0 0         my ($our_side, $their_side) = $rel->{is_right_entity} ? qw/ right left / : qw/ left right /;
347              
348 0 0         $data = [$data] unless ref $data eq 'ARRAY';
349              
350 0           foreach my $entity (@$data) {
351              
352             die "remove_related() error: give me an instance of '$rel->{entity}' or an arrayref of it."
353 0 0 0       unless blessed $entity && $entity->isa('DBIx::EAV::Entity') && $entity->type->name eq $rel->{entity};
      0        
354              
355             $relationships_table->delete({
356             relationship_id => $rel->{id},
357 0           $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   my $self = shift;
366              
367 0           while (my ($k, $v) = each %{$self->_modified}) {
  0            
368 0           $self->raw->{$k} = $v;
369 0           delete $self->raw->{$k};
370             }
371              
372 0           $self;
373             }
374              
375              
376             sub delete {
377 0     0 1   my $self = shift;
378 0 0         die "Can't delete coz I'm not in storage!"
379             unless $self->in_storage;
380              
381 0           my $eav = $self->eav;
382 0           my $type = $self->type;
383              
384             # cascade delete child entities
385 0           foreach my $rel ($type->relationships) {
386              
387             next if $rel->{is_right_entity}
388             || $rel->{is_many_to_many}
389 0 0 0       || (exists $rel->{cascade_delete} && $rel->{cascade_delete} == 0);
      0        
      0        
390              
391 0           my $rs = $self->_get_related($rel->{name});
392 0           while (my $related_entity = $rs->next) {
393 0           $related_entity->delete;
394             }
395             }
396              
397 0 0         unless ($eav->schema->database_cascade_delete) {
398              
399             # delete relationship links
400 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           my %data_types = map { $_->{data_type} => 1 }
  0            
407             $type->attributes( no_static => 1 );
408              
409 0           foreach my $data_type (keys %data_types) {
410 0           $eav->table('value_'.$data_type)->delete({ entity_id => $self->id });
411             }
412             }
413              
414             # delete entity
415 0           my $entities_table = $self->eav->table('entities');
416 0           my $rv = $entities_table->delete({ id => $self->id });
417 0           delete $self->raw->{id}; # not in_storage
418 0           $rv;
419             }
420              
421              
422             ## ##
423             ## Class Methods ##
424             ## ##
425              
426             sub is_custom_class {
427 0     0 0   my $class = shift;
428 0 0         croak "is_custom_class() is a Class method." if ref $class;
429 0           $class ne __PACKAGE__;
430             }
431              
432             sub type_definition {
433 0     0 0   my $class = shift;
434              
435 0 0         croak "type_definition() is a Class method." if ref $class;
436 0 0         croak "type_definition() must be called on DBIx::EAV::Entity subclasses."
437             unless $class->is_custom_class;
438              
439 10     10   49 no strict 'refs';
  10         10  
  10         1271  
440 0 0         unless (defined *{"${class}::__TYPE_DEFINITION__"}) {
  0            
441              
442 0           my %definition;
443             # detect parent entity
444 0           my $parent_class = ${"${class}::ISA"}[0];
  0            
445 0 0         ($definition{extends}) = $parent_class =~ /::(\w+)$/
446             if $parent_class ne __PACKAGE__;
447              
448 0           *{"${class}::__TYPE_DEFINITION__"} = \%definition;
  0            
449             }
450              
451              
452 0           \%{"${class}::__TYPE_DEFINITION__"};
  0            
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   39 no strict 'refs';
  10         10  
  10         1149  
458             *{$stuff} = sub {
459 0     0     my ($class, $spec) = @_;
460              
461 0 0         croak "$stuff() is a Class method." if ref $class;
462 0 0         croak "$stuff() must be called on DBIx::EAV::Entity subclasses."
463             unless $class->is_custom_class;
464              
465 0 0         my $key = $stuff eq 'attribute' ? 'attributes' : $stuff;
466 0           push @{ $class->type_definition->{$key} }, $spec;
  0            
467             };
468             }
469              
470              
471              
472             1;
473              
474              
475             __END__