File Coverage

lib/DBIx/EAV.pm
Criterion Covered Total %
statement 34 36 94.4
branch n/a
condition n/a
subroutine 12 12 100.0
pod n/a
total 46 48 95.8


line stmt bran cond sub pod time code
1             package DBIx::EAV;
2              
3 10     10   1216178 use Moo;
  10         81975  
  10         42  
4 10     10   14130 use strictures 2;
  10         10990  
  10         323  
5 10     10   1593 use DBI;
  10         20  
  10         284  
6 10     10   6840 use Lingua::EN::Inflect ();
  10         179349  
  10         399  
7 10     10   67 use Data::Dumper;
  10         12  
  10         473  
8 10     10   3058 use DBIx::EAV::EntityType;
  10         19  
  10         279  
9 10     10   3424 use DBIx::EAV::Entity;
  10         17  
  10         263  
10 10     10   3178 use DBIx::EAV::ResultSet;
  10         169  
  10         304  
11 10     10   3188 use DBIx::EAV::Schema;
  10         22  
  10         359  
12 10     10   51 use Carp qw' croak confess ';
  10         11  
  10         550  
13 10     10   39 use Scalar::Util 'blessed';
  10         12  
  10         369  
14 10     10   10228 use Class::Load qw' try_load_class ';
  0            
  0            
15             use namespace::clean;
16              
17             our $VERSION = "0.08";
18              
19             # required
20             has 'dbh', is => 'ro', required => 1;
21              
22             # options
23             has 'default_attribute_type', is => 'ro', default => 'varchar';
24             has 'schema_config', is => 'ro', default => sub { {} };
25             has 'entity_namespaces', is => 'ro', default => sub { [] };
26             has 'resultset_namespaces', is => 'ro', default => sub { [] };
27              
28             # internal
29             has 'schema', is => 'ro', lazy => 1, builder => 1, init_arg => undef, handles => [qw/ table dbh_do /];
30             has '_types', is => 'ro', default => sub { {} };
31             has '_types_by_id', is => 'ro', default => sub { {} };
32              
33             # group schema_config params
34             around BUILDARGS => sub {
35             my ( $orig, $class, @args ) = @_;
36             my $params = @args == 1 && ref $args[0] ? $args[0] : { @args };
37             my $schema_config = delete $params->{schema_config} || {};
38              
39             my @schema_params = grep { exists $params->{$_} } qw/
40             tenant_id data_types database_cascade_delete static_attributes
41             table_prefix id_type default_attribute_type
42             /;
43              
44             @{$schema_config}{@schema_params} = delete @{$params}{@schema_params};
45              
46             $class->$orig(%$params, schema_config => $schema_config);
47             };
48              
49              
50             sub _build_schema {
51             my $self = shift;
52             DBIx::EAV::Schema->new(%{$self->schema_config}, dbh => $self->dbh);
53             }
54              
55             sub connect {
56             my ($class, $dsn, $user, $pass, $attrs, $constructor_params) = @_;
57              
58             croak 'Missing $dsn argument for connect()' unless $dsn;
59              
60             croak "connect() must be called as a class method."
61             if ref $class;
62              
63             $constructor_params //= {};
64              
65             $constructor_params->{dbh} = DBI->connect($dsn, $user, $pass, $attrs)
66             or die $DBI::errstr;
67              
68             $class->new($constructor_params);
69             }
70              
71             sub type {
72             my ($self, $value) = @_;
73              
74             return $self->_types->{$value}
75             if exists $self->_types->{$value};
76              
77             my $type = $self->_load_type('name', $value);
78              
79             # not registered, try to find a custom entity class and register it
80             if (!$type && (my $entity_class = $self->_resolve_entity_class($value))) {
81              
82             ($type) = $self->register_types({$value => $entity_class->type_definition});
83             }
84              
85             confess "EntityType '$value' does not exist."
86             unless $type;
87              
88             $type;
89             }
90              
91             sub type_by_id {
92             my ($self, $value) = @_;
93              
94             return $self->_types_by_id->{$value}
95             if exists $self->_types_by_id->{$value};
96              
97             $self->_load_type('id', $value)
98             or confess "EntityType 'id=$value' does not exist.";
99             }
100              
101             sub _load_type {
102             my ($self, $field, $value) = @_;
103              
104             my $type_row = $self->table('entity_types')->select_one({ $field => $value });
105             return unless $type_row;
106              
107             my $type = DBIx::EAV::EntityType->load({ %$type_row, core => $self});
108             $self->_types->{$type->name} = $type;
109             $self->_types_by_id->{$type->id} = $type;
110             $type;
111             }
112              
113             sub _resolve_entity_class {
114             my ($self, $name) = @_;
115              
116             foreach my $ns (@{ $self->entity_namespaces }) {
117              
118             my $entity_class = join '::', $ns, $name;
119             my ($is_loaded, $error) = try_load_class $entity_class;
120              
121             return $entity_class if $is_loaded;
122              
123             # rethrow compilation errors
124             die $error if $error =~ /^Can't locate .* in \@INC/;
125             }
126              
127             return;
128             }
129              
130             sub _resolve_resultset_class {
131             my ($self, $name) = @_;
132              
133             foreach my $ns (@{ $self->resultset_namespaces }) {
134              
135             my $class = join '::', $ns, $name;
136             my ($is_loaded, $error) = try_load_class $class;
137              
138             return $class if $is_loaded;
139              
140             # rethrow compilation errors
141             die $class;
142             }
143              
144             return;
145             }
146              
147             sub resultset {
148             my ($self, $name) = @_;
149              
150             my $rs_class = $self->_resolve_resultset_class($name)
151             || 'DBIx::EAV::ResultSet';
152              
153             $rs_class->new({
154             eav => $self,
155             type => $self->type($name),
156             });
157             }
158              
159              
160             sub register_types {
161             my ($self, $schema) = @_;
162             my %skip;
163              
164             # register only not-installed entities to
165             # allow multiple calls to this method
166             my @new_types = grep { not exists $self->_types->{$_} } keys %$schema;
167              
168             # create or update each entity type on database
169             my @registered_types;
170             foreach my $name (@new_types) {
171             next if exists $self->_types->{$name};
172             push @registered_types,
173             $self->_register_entity_type($name, $schema->{$name}, $schema);
174             }
175              
176             # register relationships
177             foreach my $name (@new_types) {
178              
179             my $spec = $schema->{$name};
180             my $entity_type = $self->type($name);
181              
182             foreach my $reltype (qw/ has_one has_many many_to_many /) {
183              
184             next unless defined $spec->{$reltype};
185              
186             $spec->{$reltype} ||= [];
187             $spec->{$reltype} = [$spec->{$reltype}]
188             unless ref $spec->{$reltype} eq 'ARRAY';
189              
190             foreach my $rel (@{$spec->{$reltype}}) {
191             $entity_type->register_relationship($reltype, $rel);
192             }
193             }
194             }
195              
196             @registered_types;
197             }
198              
199              
200             sub _register_entity_type {
201             my ($self, $name, $spec, $schema) = @_;
202              
203             # parent type first
204             my $parent_type;
205             if ($spec->{extends}) {
206              
207             unless ($parent_type = $self->_types->{$spec->{extends}}) {
208              
209             die "Unknown type '$spec->{extends}' specified in 'extents' option for type '$name'."
210             unless exists $schema->{$spec->{extends}};
211              
212             $parent_type = $self->_register_entity_type($spec->{extends}, $schema->{$spec->{extends}}, $schema);
213             }
214             }
215              
216             # find or create entity type
217             my $types_table = $self->table('entity_types');
218             my $hierarchy_table = $self->table('type_hierarchy');
219             my $type = $types_table->select_one({ name => $name });
220              
221             if (defined $type) {
222              
223             # change parent
224             }
225             else {
226              
227             # TODO implement rename
228             # if ($spec->{rename_from}) { ... }
229              
230             my $id = $types_table->insert({ name => $name });
231             $type = $types_table->select_one({ id => $id });
232             die "Error inserting entity type '$name'!" unless $type;
233              
234             if ($parent_type) {
235             $hierarchy_table->insert({
236             parent_type_id => $parent_type->{id},
237             child_type_id => $type->{id}
238             });
239              
240             $type->{parent} = $parent_type;
241             }
242             }
243              
244             # update or create attributes
245             my $attributes = $self->table('attributes');
246             my %static_attributes = map { $_ => {name => $_, is_static => 1} } @{$self->table('entities')->columns};
247             $type->{attributes} = {};
248              
249             my %inherited_attributes = $parent_type ? map { $_->{name} => $_ } $parent_type->attributes( no_static => 1 )
250             : ();
251              
252             foreach my $attr_spec (@{$spec->{attributes}}) {
253              
254             # expand string to name/type
255             unless (ref $attr_spec) {
256             my ($name, $type) = split ':', $attr_spec;
257             $attr_spec = {
258             name => $name,
259             type => $type || $self->default_attribute_type
260             };
261             }
262              
263             die sprintf("Error registering attribute '%s' for entity '%s'. Can't use names of static attributes (real table columns).", $attr_spec->{name}, $type->{name})
264             if exists $static_attributes{$attr_spec->{name}};
265              
266             printf STDERR "[warn] entity '%s' is overriding inherited attribute '%s'", $name, $attr_spec->{name}
267             if $inherited_attributes{$attr_spec->{name}};
268              
269             my $attr = $attributes->select_one({
270             entity_type_id => $type->{id},
271             name => $attr_spec->{name}
272             });
273              
274             if (defined $attr) {
275             # update
276             }
277             else {
278             delete $attr_spec->{id}; # safety
279              
280             my %data = %$attr_spec;
281              
282             $data{entity_type_id} = $type->{id};
283             $data{data_type} = delete($data{type}) || $self->default_attribute_type;
284              
285             die sprintf("Attribute '%s' has unknown data type '%s'.", $data{name}, $data{data_type})
286             unless $self->schema->has_data_type($data{data_type});
287              
288             $attributes->insert(\%data);
289             $attr = $attributes->select_one(\%data);
290             die "Error inserting attribute '$attr_spec->{name}'!" unless $attr;
291             }
292              
293             $type->{attributes}{$attr->{name}} = $attr;
294             }
295              
296             $self->_types->{$name} =
297             $self->_types_by_id->{$type->{id}} = DBIx::EAV::EntityType->new(%$type, core => $self);
298             }
299              
300              
301              
302             1;
303              
304             __END__