File Coverage

blib/lib/StoredHash/ISA.pm
Criterion Covered Total %
statement 40 116 34.4
branch 7 42 16.6
condition n/a
subroutine 9 17 52.9
pod 7 9 77.7
total 63 184 34.2


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             StoredHash::ISA - Allow Object to be-a StoredHash by automatically inheriting persistence abilities.
4              
5             =head1 DESCRIPTION
6              
7             StoredHash::ISA allows to create an IS-A (ISA in Perl lingo) relationship between any object class
8             and StoredHash persister. This allows you to call StoredHash methods directly via instance
9             (or as Class methods where appropriate).
10              
11             Because StoredHash::ISA bases persistence operations on class introspection, the persisted
12             objects must belong to a class package and not be "plain" HASHes (You'd use StoredHash).
13             Even when some methods are overloaded to work as Instance or class methods, the Objects
14             must be blessed.
15              
16             Using StoredHash::ISA as a base class allows you to use following persistence methods
17              
18             =over 4
19              
20             =item * insert() - as instance method
21              
22             =item * update() - as instance method
23              
24             =item * load() - as class method
25              
26             =item * loadset() - as class method
27              
28             =item * delete() - as instance method - or class method
29              
30             =item * exists() - as class method
31              
32             =item * reload() - StoredHash::ISA custom instance method to reload instance from db
33              
34             =back
35              
36             =cut
37             # Stripped
38             # insert (or class method)
39             # update (or class method)
40             # load (or instance method to "reload")
41             # exists (or instance method)
42             =pod
43              
44             The ways of using methods (class vs. instance) above.
45             Perl generally allows class methods (i.e. non-instance methods) to be called with two syntaxes
46             (with major underlying differences):
47              
48             ThePack::a_method()
49             # And
50             ThePack-> a_method()
51              
52             All StoredHash::ISA class methods (as listed above) need to be called using ThePack-> a_method() (this is related to
53             giving framework a hint about objects type).
54              
55             =head1 SYNOPSIS
56              
57             Package wanting to be-a persister:
58              
59             {
60             package Justanother::Object;
61             # Inherit persister methods
62             # our @ISA = ('StoredHash::ISA'); # Or more by more modern style ...
63             use base ('StoredHash::ISA');
64             # Must declare
65             our $shp = {'table' => 'another', 'pkey' => ['id'], 'autoid' => 1,}
66             # Custom functionality ... methods as usual
67            
68             }
69              
70             using the class ...
71              
72             my $o = Justanother::Object->new('prop' => 'The Value',);
73             my $id = $o->insert();
74            
75             # Load object
76             my $o = Justanother::Object->load([46]);
77             # Load related children (blessed automatically)
78             my $o->{'items'} = Justanother::Object::Children->loadset({'parent' => 46});
79            
80             # Setting up Your inheriting class-package for persistence
81             {
82             package Justanother::Object;
83            
84             our $shp = {'table' => 'anotherobj', ...};
85             use base 'StoredHash::ISA'; # Same as our @ISA = ('StoredHash::ISA');
86             }
87              
88             =cut
89              
90             # package Justanother::Object;
91             # At import Tweak (bless) our $shp of requesting class
92             # use Storedhash;
93             # use Storedhash::ISA;
94             # our @ISA = ('StoredHash::ISA');
95              
96             # This only puts one more burden on the application - hashes must always be blessed hashes -
97             # not plain
98             # unblessed ones. On the other hasn when they are retrieved from DB with autoconnected methods:
99             # -
100             # -
101             # They will always be automatically blessed.
102             # The scenario that you have to watch for is when getting a raw hash from Desktop app or Web form in
103             # unblessed, form - it must be blessed to class before calling methods via it.
104              
105              
106             # DEV:
107             # Whatever way the persistence is implemented The StoredHash or StoredHash::ISA Must somehow get to
108             # know what persister to use
109              
110             # Using original StoredHash methods (Benefit-no new classes):
111             # sub insert {
112             # my ($p, $h) = @_;
113             # if (($c = ref($p)) ne 'StoredHash') {
114             # $h = $p; # Make Calling abject THE hash
115             # # Lookup persister from "Class Table"
116             # $p = $StoredHash::clt->{$c};
117             # # OR Class itself !!!
118             # $p = ${"$c"}::shp;
119             #}
120             # ...
121             #}
122              
123             # Implement _relevant_methods of
124              
125             #our @ISA
126             package StoredHash::ISA;
127 2     2   7213 use StoredHash;
  2         4  
  2         56  
128             # blessed gets the ...
129 2     2   8 use Scalar::Util('reftype','blessed');
  2         3  
  2         98  
130 2     2   7 use strict;
  2         3  
  2         54  
131 2     2   8 use warnings;
  2         13  
  2         55  
132              
133 2     2   6 use Data::Dumper;
  2         2  
  2         285  
134             our $debug = 0;
135             our $VERSION = '0.30';
136             $Data::Dumper::Indent = 1;
137             $Data::Dumper::Terse = 1;
138             # Cache Mapping from classes to persister (to avoid lookup to the class itself)
139             # The internal import-time boot() -method registers classes in here.
140             my $clt = {};
141             our @methods = ('insert','update','delete','load','loadset','exists',);
142             # Safe methods (no 'delete' and 'exists')
143             our @safemethods = ('insert','update','load','loadset',);
144              
145             # Perl standard import method for StoredHash::ISA.
146             # This is auto-triggered when class loads StoredHash::ISA by "use StoredHash::ISA;".
147             # This calls boot() to carry out some of the setup.
148             sub import {
149 2     2   12 my ($cl) = @_;
150 2         12 my @ci = caller(0);
151 2 50       11 if ($debug) {print(STDERR Dumper(\@ci));}
  0         0  
152 2     2   9 no strict 'refs';
  2         2  
  2         1335  
153             # Grab $shp from callers package
154             # TODO: Convert to symbol table lookup
155 2         4 my $ssym = "$ci[0]\:\:shp";
156 2         2 my $shp = ${$ssym}; # eval('$'.$ci[0].'::shp'); #
  2         6  
157             #DEBUG:print("#$ssym#\n");print(Dumper($shp));
158             #if (!$shp) {$shp = '';} # undef does not work for strict/warnings
159             # TODO: Use reftype()
160 2 100       7 if (!$shp) {die("No persister info for package '$ci[0]'");}
  1         14  
161 1 50       6 if (reftype($shp) ne 'HASH') {die("Persister info for package '$ci[0]' NOT in a HASH ($shp)");}
  0         0  
162             #StoredHash::validate($shp); # Throws errors
163 1         2 boot($shp, $ci[0]); # 'class' =>
164 1 50       2 if ($debug) {print(Dumper($shp));}
  0         0  
165 1 50       24 if ($debug) {print("CLT ".Dumper($clt));}
  0         0  
166             }
167              
168             #=head2 boot($shp, $class)
169              
170             # Bootstrap StoredHash configuration embedded into the class loading the StoredHash::ISA.
171             #
172             #=item * Make sure the StoredHash config-declaration (with 'table', 'pkey' ...) is blessed to StoredHash
173             #=item * Attach / import methods to original class
174             #=item * Register class to class-to-persister mapping table maintained here.
175             #=cut
176             sub boot {
177 1     1 0 1 my ($shp, $c) = @_; # %c
178 1 50       3 if (!blessed($shp)) {bless($shp, 'StoredHash');}
  1         2  
179 1         5 $shp->{'class'} = $c; # Force class = $class
180             # Methods OR safemethods
181             # NOTE: This should be optional as @ISA method dispatching takes care of this.
182 1         2 map({eval("*${c}::$_ = \\&StoredHash::ISA::$_");} @methods); # $c{'class'}
  6         238  
183 1         3 $clt->{$c} = $shp; # $c{'class'}
184            
185             }
186              
187             =head1 METHODS
188              
189             Implementations as distinct instance methods
190              
191             =head2 my ($id) = $e->insert(%opts)
192              
193             Insert an instance of a class to database.
194             Return id(s) as a array / list (real array that is).
195              
196             =cut
197             sub insert {
198 0     0 1   my ($h, %c) = @_;
199 0           my $c;
200             # Called as class method. Allow this somewhat ugly overloading ?
201 0 0         if (reftype($h) ne 'HASH') {
  0            
202 0           ($c, $h, %c) = @_;
203             #die("StoredHash::ISA: Only works with hash Objects");
204             }
205             else {$c = blessed($h);}
206 0           my $p = $clt->{$c};
207             # Ensure this is a StoredHash $p->isa('StoredHash');
208 0 0         if (!$p) {die("Persister not resolved for '$c'");}
  0            
209 0           return $p->insert($h, %c);
210             }
211             # OLD:
212             # Probe caller() to to insert / update
213             # Swap the roles of $p and $h
214             # TODO: Allow Class / Inst
215              
216             =head2 $entry->update($ids)
217              
218             Update entry in database. Allows using explicit 'attrs' to minimize attributes to be updated.
219             Return true value on success. Throw exception on failure.
220              
221             =cut
222             sub update {
223 0     0 1   my ($h, $ids, %c) = @_;
224 0 0         if (reftype($h) ne 'HASH') {die("StoredHash::ISA: Only works with hash Objects");}
  0            
225             # Allow entry to contain IDs ? See reload() for example.
226 0 0         if (!$ids) {}
227 0 0         if (reftype($ids) ne 'ARRAY') {die("ID not in an ARRAY");}
  0            
228 0           my $c = blessed($h);
229 0           my $p = $clt->{$c};
230 0           $p->update($h, $ids, %c);
231             }
232              
233             =head2 $entry->delete($ids)
234              
235             Delete an instance of an entry from DB.
236             Note that as the persisted version ceases to exist, probably the runtime instance should as well.
237              
238             $entry->delete($ids)
239             undef($entry);
240              
241             =cut
242             # TODO: Allow to work as Class or instance method: MyType->delete();
243             # TODO:
244             sub delete {
245 0     0 1   my ($h, $ids, %c) = @_;
246 0           my $c;
247 0           my $isinst = reftype($h) eq 'HASH';
248             # Support instance BUT w/o $ids: $e->delete()
249             #TODO:if ($isinst && !$ids) {$ids = embedded_ids($p, $e);}
250             # Support Class method call: MyType->delete($ids). Re-shuffle stack params slightly.
251             # param $_[0] (class) must be found in the class table.
252             #TODO:if (!$isinst && $clt->{$_[0]}) {$c = $h;goto ANYDELETE;}
253 0 0         if (!$isinst) {die("No (hash based) instance (and not a class call)");}
  0            
254            
255 0           $c = blessed($h); # Declared above to allow stack
256 0           ANYDELETE:
257             # Do this validation late (mainly for case Class call)
258 0 0         if (reftype($ids) ne 'ARRAY') {die("ID(s) not in an ARRAY");}
259 0           my $p = $clt->{$c};
260             ##TODO: if (!$ids) {$ids = embedded_ids($p, $e);}
261 0           $p->delete($h, $ids, %c);
262             # Ok as Enforced ?
263 0 0         if ($isinst) {$_[0] = undef;}
  0            
264             }
265              
266             =head2 $e = MyType->load($ids)
267              
268             Class Method to load an entry of particular type from DB.
269             Return (blessed) entry.
270            
271             =cut
272             # TODO: Consider the usage instance method to "reload")
273             sub load {
274 0     0 1   my ($c, $ids, %c) = @_;
275             #if (reftype($c) eq 'HASH') {}
276 0           my $p = $clt->{$c};
277 0           my $e = $p->load($ids, %c);
278             # Is this redundant - entry already blessed ?
279 0           return bless($e, $c);
280             }
281             =head2 $e->reload($ids)
282              
283             Reload entry instance from database.
284             $ids is optional as long as entry contains the id attribute values.
285             Return (blessed) entry.
286              
287             =cut
288             # TODO: Define the behaviour for setting $_[0] in callstack
289             sub reload {
290 0     0 1   my ($e, $ids, %c) = @_;
291 0           my $c = blessed($e);
292 0 0         if (!$c) {die("Not a blessed object");}
  0            
293 0           my $p = $clt->{$c};
294             # No explicit ID, must be in the entry. Discover them.
295 0 0         if (!$ids) {
296 0           my @pkv = $p->pkeyvals($e);
297 0           my @pka = $p->pkeys();
298 0 0         if (@pkv ne @pka) {die("ID attrs / vals - not matching");}
  0            
299 0           $ids = \@pkv; # Use "discovered" IDs
300             #TODO: $ids = embedded_ids($p, $e);
301             }
302             # This would not overwrite callers instance (assigning to $_[0] will)
303 0           $e = $p->load($ids, %c);
304 0           bless($e, $c);
305 0           $_[0] = $e; # Optional "replace in stack" ?
306 0           return($e);
307             }
308              
309             =head2 MyType->loadset($filter, $sortattrs, %opts)
310              
311             Class method to load a set of entries for a class from the database.
312              
313             =cut
314             sub loadset {
315 0     0 1   my ($c, $wf, $o) = @_;
316 0           my $p = $clt->{$c};
317 0 0         if (!$p) {die("No persister for class '$c'");}
  0            
318 0           my $arr = $p->loadset($wf, $o);
319             # Test autobless config (for class)
320 0           my $abv = "$c\:\:noautobless";
321 2     2   28 no strict ('refs');
  2         3  
  2         346  
322             # no auto bless - return unblessed
323 0 0         if (${$abv}) {return($arr);}
  0            
  0            
324 0           return [map({ bless($_, $c); } @$arr)];
  0            
325             }
326              
327             =head2 MyType->exists($ids)
328              
329             Class method to test if an instance exists in database.
330             Return true for "does exist", false for "not".
331              
332             =cut
333             sub exists {
334 0     0 1   my ($c, $ids, %c) = @_;
335 0           my $p = $clt->{$c};
336 0           $p->exists($ids, %c);
337             }
338              
339             #=head2 $ids = embedded_ids($shp, $e)
340             #Try to discover (DB) id(s) for methods that allow leaving out $ids from parameters.
341             #The discovery must be prefect to be valid.
342             #Return $ids (as arrayref), throw exception on any failures.
343             #=cut
344             sub embedded_ids {
345 0     0 0   my ($p, $e) = @_;
346 0           my @pkv = $p->pkeyvals($e);
347 0           my @pka = $p->pkeys();
348 0 0         if (grep({!$_;} @pkv)) {die("ID:s cannot be empty or have a non-true value.");}
  0            
  0            
349 0 0         if (@pkv ne @pka) {die("ID attrs / vals - not matching");}
  0            
350 0           return \@pkv; # Use "discovered" IDs
351             }
352              
353             1;