]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - classes/Profile_list.php
Cache rollup stuff in the cache, not in the DB
[quix0rs-gnu-social.git] / classes / Profile_list.php
1 <?php
2 /**
3  * StatusNet - the distributed open-source microblogging tool
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU Affero General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.     See the
13  * GNU Affero General Public License for more details.
14  *
15  * You should have received a copy of the GNU Affero General Public License
16  * along with this program.     If not, see <http://www.gnu.org/licenses/>.
17  *
18  * @category Notices
19  * @package  StatusNet
20  * @author   Shashi Gowda <connect2shashi@gmail.com>
21  * @license  GNU Affero General Public License http://www.gnu.org/licenses/
22  */
23
24 if (!defined('STATUSNET') && !defined('LACONICA')) {
25     exit(1);
26 }
27
28 /**
29  * Table Definition for profile_list
30  */
31 require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
32
33 class Profile_list extends Memcached_DataObject
34 {
35     ###START_AUTOCODE
36     /* the code below is auto generated do not remove the above tag */
37
38     public $__table = 'profile_list';                      // table name
39     public $id;                              // int(4)  primary_key not_null
40     public $tagger;                          // int(4)
41     public $tag;                             // varchar(64)
42     public $description;                     // text
43     public $private;                         // tinyint(1)
44     public $created;                         // datetime   not_null default_0000-00-00%2000%3A00%3A00
45     public $modified;                        // timestamp   not_null default_CURRENT_TIMESTAMP
46     public $uri;                             // varchar(255)  unique_key
47     public $mainpage;                        // varchar(255)
48     public $tagged_count;                    // smallint
49     public $subscriber_count;                // smallint
50
51     /* Static get */
52     function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('Profile_list',$k,$v); }
53
54     /* the code above is auto generated do not remove the tag below */
55     ###END_AUTOCODE
56
57     /**
58      * return a profile_list record, given its tag and tagger.
59      *
60      * @param array $kv ideally array('tag' => $tag, 'tagger' => $tagger)
61      *
62      * @return Profile_list a Profile_list object with the given tag and tagger.
63      */
64
65     function pkeyGet($kv)
66     {
67         return Memcached_DataObject::pkeyGet('Profile_list', $kv);
68     }
69
70     /**
71      * get the tagger of this profile_list object
72      *
73      * @return Profile the tagger
74      */
75
76     function getTagger()
77     {
78         return Profile::staticGet('id', $this->tagger);
79     }
80
81     /**
82      * return a string to identify this
83      * profile_list in the user interface etc.
84      *
85      * @return String
86      */
87
88     function getBestName()
89     {
90         return $this->tag;
91     }
92
93     /**
94      * return a uri string for this profile_list
95      *
96      * @return String uri
97      */
98
99     function getUri()
100     {
101         $uri = null;
102         if (Event::handle('StartProfiletagGetUri', array($this, &$uri))) {
103             if (!empty($this->uri)) {
104                 $uri = $this->uri;
105             } else {
106                 $uri = common_local_url('profiletagbyid',
107                                         array('id' => $this->id, 'tagger_id' => $this->tagger));
108             }
109         }
110         Event::handle('EndProfiletagGetUri', array($this, &$uri));
111         return $uri;
112     }
113
114     /**
115      * return a url to the homepage of this item
116      *
117      * @return String home url
118      */
119
120     function homeUrl()
121     {
122         $url = null;
123         if (Event::handle('StartUserPeopletagHomeUrl', array($this, &$url))) {
124             // normally stored in mainpage, but older ones may be null
125             if (!empty($this->mainpage)) {
126                 $url = $this->mainpage;
127             } else {
128                 $url = common_local_url('showprofiletag',
129                                         array('tagger' => $this->getTagger()->nickname,
130                                               'tag'    => $this->tag));
131             }
132         }
133         Event::handle('EndUserPeopletagHomeUrl', array($this, &$url));
134         return $url;
135     }
136
137     /**
138      * return an immutable url for this object
139      *
140      * @return String permalink
141      */
142
143     function permalink()
144     {
145         $url = null;
146         if (Event::handle('StartProfiletagPermalink', array($this, &$url))) {
147             $url = common_local_url('profiletagbyid',
148                                     array('id' => $this->id));
149         }
150         Event::handle('EndProfiletagPermalink', array($this, &$url));
151         return $url;
152     }
153
154     /**
155      * Query notices by users associated with this tag,
156      * but first check the cache before hitting the DB.
157      *
158      * @param integer $offset   offset
159      * @param integer $limit    maximum no of results
160      * @param integer $since_id=null    since this id
161      * @param integer $max_id=null  maximum id in result
162      *
163      * @return Notice the query
164      */
165
166     function getNotices($offset, $limit, $since_id=null, $max_id=null)
167     {
168         $stream = new PeopletagNoticeStream($this);
169
170         return $stream->getNotices($offset, $limit, $since_id, $max_id);
171     }
172
173     /**
174      * Query notices by users associated with this tag from the database.
175      *
176      * @param integer $offset   offset
177      * @param integer $limit    maximum no of results
178      * @param integer $since_id=null    since this id
179      * @param integer $max_id=null  maximum id in result
180      *
181      * @return array array of notice ids.
182      */
183
184     function _streamDirect($offset, $limit, $since_id, $max_id)
185     {
186         $inbox = new Profile_tag_inbox();
187
188         $inbox->profile_tag_id = $this->id;
189
190         $inbox->selectAdd();
191         $inbox->selectAdd('notice_id');
192
193         if ($since_id != 0) {
194             $inbox->whereAdd('notice_id > ' . $since_id);
195         }
196
197         if ($max_id != 0) {
198             $inbox->whereAdd('notice_id <= ' . $max_id);
199         }
200
201         $inbox->orderBy('notice_id DESC');
202
203         if (!is_null($offset)) {
204             $inbox->limit($offset, $limit);
205         }
206
207         $ids = array();
208
209         if ($inbox->find()) {
210             while ($inbox->fetch()) {
211                 $ids[] = $inbox->notice_id;
212             }
213         }
214
215         return $ids;
216     }
217
218     /**
219      * Get subscribers (local and remote) to this people tag
220      * Order by reverse chronology
221      *
222      * @param integer $offset   offset
223      * @param integer $limit    maximum no of results
224      * @param integer $since_id=null    since unix timestamp
225      * @param integer $upto=null  maximum unix timestamp when subscription was made
226      *
227      * @return Profile results
228      */
229
230     function getSubscribers($offset=0, $limit=null, $since=0, $upto=0)
231     {
232         $subs = new Profile();
233         $sub = new Profile_tag_subscription();
234         $sub->profile_tag_id = $this->id;
235
236         $subs->joinAdd($sub);
237         $subs->selectAdd('unix_timestamp(profile_tag_subscription.' .
238                          'created) as "cursor"');
239
240         if ($since != 0) {
241             $subs->whereAdd('cursor > ' . $since);
242         }
243
244         if ($upto != 0) {
245             $subs->whereAdd('cursor <= ' . $upto);
246         }
247
248         if ($limit != null) {
249             $subs->limit($offset, $limit);
250         }
251
252         $subs->orderBy('profile_tag_subscription.created DESC');
253         $subs->find();
254
255         return $subs;
256     }
257
258     /**
259      * Get all and only local subscribers to this people tag
260      * used for distributing notices to user inboxes.
261      *
262      * @return array ids of users
263      */
264
265     function getUserSubscribers()
266     {
267         // XXX: cache this
268
269         $user = new User();
270         if(common_config('db','quote_identifiers'))
271             $user_table = '"user"';
272         else $user_table = 'user';
273
274         $qry =
275           'SELECT id ' .
276           'FROM '. $user_table .' JOIN profile_tag_subscription '.
277           'ON '. $user_table .'.id = profile_tag_subscription.profile_id ' .
278           'WHERE profile_tag_subscription.profile_tag_id = %d ';
279
280         $user->query(sprintf($qry, $this->id));
281
282         $ids = array();
283
284         while ($user->fetch()) {
285             $ids[] = $user->id;
286         }
287
288         $user->free();
289
290         return $ids;
291     }
292
293     /**
294      * Check to see if a given profile has
295      * subscribed to this people tag's timeline
296      *
297      * @param mixed $id User or Profile object or integer id
298      *
299      * @return boolean subscription status
300      */
301
302     function hasSubscriber($id)
303     {
304         if (!is_numeric($id)) {
305             $id = $id->id;
306         }
307
308         $sub = Profile_tag_subscription::pkeyGet(array('profile_tag_id' => $this->id,
309                                                        'profile_id'     => $id));
310         return !empty($sub);
311     }
312
313     /**
314      * Get profiles tagged with this people tag,
315      * include modified timestamp as a "cursor" field
316      * order by descending order of modified time
317      *
318      * @param integer $offset   offset
319      * @param integer $limit    maximum no of results
320      * @param integer $since_id=null    since unix timestamp
321      * @param integer $upto=null  maximum unix timestamp when subscription was made
322      *
323      * @return Profile results
324      */
325
326     function getTagged($offset=0, $limit=null, $since=0, $upto=0)
327     {
328         $tagged = new Profile();
329         $tagged->joinAdd(array('id', 'profile_tag:tagged'));
330
331         #@fixme: postgres
332         $tagged->selectAdd('unix_timestamp(profile_tag.modified) as "cursor"');
333         $tagged->whereAdd('profile_tag.tagger = '.$this->tagger);
334         $tagged->whereAdd("profile_tag.tag = '{$this->tag}'");
335
336         if ($since != 0) {
337             $tagged->whereAdd('cursor > ' . $since);
338         }
339
340         if ($upto != 0) {
341             $tagged->whereAdd('cursor <= ' . $upto);
342         }
343
344         if ($limit != null) {
345             $tagged->limit($offset, $limit);
346         }
347
348         $tagged->orderBy('profile_tag.modified DESC');
349         $tagged->find();
350
351         return $tagged;
352     }
353
354     /**
355      * Gracefully delete one or many people tags
356      * along with their members and subscriptions data
357      *
358      * @return boolean success
359      */
360
361     function delete()
362     {
363         // force delete one item at a time.
364         if (empty($this->id)) {
365             $this->find();
366             while ($this->fetch()) {
367                 $this->delete();
368             }
369         }
370
371         Profile_tag::cleanup($this);
372         Profile_tag_subscription::cleanup($this);
373
374         return parent::delete();
375     }
376
377     /**
378      * Update a people tag gracefully
379      * also change "tag" fields in profile_tag table
380      *
381      * @param Profile_list $orig    Object's original form
382      *
383      * @return boolean success
384      */
385
386     function update($orig=null)
387     {
388         $result = true;
389
390         if (!is_object($orig) && !$orig instanceof Profile_list) {
391             parent::update($orig);
392         }
393
394         // if original tag was different
395         // check to see if the new tag already exists
396         // if not, rename the tag correctly
397         if($orig->tag != $this->tag || $orig->tagger != $this->tagger) {
398             $existing = Profile_list::getByTaggerAndTag($this->tagger, $this->tag);
399             if(!empty($existing)) {
400                 // TRANS: Server exception.
401                 throw new ServerException(_('The tag you are trying to rename ' .
402                                             'to already exists.'));
403             }
404             // move the tag
405             // XXX: allow OStatus plugin to send out profile tag
406             $result = Profile_tag::moveTag($orig, $this);
407         }
408         parent::update($orig);
409         return $result;
410     }
411
412     /**
413      * return an xml string representing this people tag
414      * as the author of an atom feed
415      *
416      * @return string atom author element
417      */
418
419     function asAtomAuthor()
420     {
421         $xs = new XMLStringer(true);
422
423         $tagger = $this->getTagger();
424         $xs->elementStart('author');
425         $xs->element('name', null, '@' . $tagger->nickname . '/' . $this->tag);
426         $xs->element('uri', null, $this->permalink());
427         $xs->elementEnd('author');
428
429         return $xs->getString();
430     }
431
432     /**
433      * return an xml string to represent this people tag
434      * as the subject of an activitystreams feed.
435      *
436      * @return string activitystreams subject
437      */
438
439     function asActivitySubject()
440     {
441         return $this->asActivityNoun('subject');
442     }
443
444     /**
445      * return an xml string to represent this people tag
446      * as a noun in an activitystreams feed.
447      *
448      * @param string $element the xml tag
449      *
450      * @return string activitystreams noun
451      */
452
453     function asActivityNoun($element)
454     {
455         $noun = ActivityObject::fromPeopletag($this);
456         return $noun->asString('activity:' . $element);
457     }
458
459     /**
460      * get the cached number of profiles tagged with this
461      * people tag, re-count if the argument is true.
462      *
463      * @param boolean $recount  whether to ignore cache
464      *
465      * @return integer count
466      */
467
468     function taggedCount($recount=false)
469     {
470         $keypart = sprintf('profile_list:tagged_count:%d:%s', 
471                            $this->tagger,
472                            $this->tag);
473
474         $count = self::cacheGet($keypart);
475
476         if ($count === false) {
477             $tags = new Profile_tag();
478
479             $tags->tag = $this->tag;
480             $tags->tagger = $this->tagger;
481
482             $count = $tags->count('distinct tagged');
483
484             self::cacheSet($keypart, $count);
485         }
486
487         return $count;
488     }
489
490     /**
491      * get the cached number of profiles subscribed to this
492      * people tag, re-count if the argument is true.
493      *
494      * @param boolean $recount  whether to ignore cache
495      *
496      * @return integer count
497      */
498
499     function subscriberCount($recount=false)
500     {
501         $keypart = sprintf('profile_list:subscriber_count:%d', 
502                            $this->id);
503
504         $count = self::cacheGet($keypart);
505
506         if ($count === false) {
507
508             $sub = new Profile_tag_subscription();
509             $sub->profile_tag_id = $this->id;
510             $count = (int) $sub->count('distinct profile_id');
511
512             self::cacheSet($keypart, $count);
513         }
514
515         return $count;
516     }
517
518     /**
519      * get the Profile_list object by the
520      * given tagger and with given tag
521      *
522      * @param integer $tagger   the id of the creator profile
523      * @param integer $tag      the tag
524      *
525      * @return integer count
526      */
527
528     static function getByTaggerAndTag($tagger, $tag)
529     {
530         $ptag = Profile_list::pkeyGet(array('tagger' => $tagger, 'tag' => $tag));
531         return $ptag;
532     }
533
534     /**
535      * create a profile_list record for a tag, tagger pair
536      * if it doesn't exist, return it.
537      *
538      * @param integer $tagger   the tagger
539      * @param string  $tag      the tag
540      * @param string  $description description
541      * @param boolean $private  protected or not
542      *
543      * @return Profile_list the people tag object
544      */
545
546     static function ensureTag($tagger, $tag, $description=null, $private=false)
547     {
548         $ptag = Profile_list::getByTaggerAndTag($tagger, $tag);
549
550         if(empty($ptag->id)) {
551             $args = array(
552                 'tag' => $tag,
553                 'tagger' => $tagger,
554                 'description' => $description,
555                 'private' => $private
556             );
557
558             $new_tag = Profile_list::saveNew($args);
559
560             return $new_tag;
561         }
562         return $ptag;
563     }
564
565     /**
566      * get the maximum number of characters
567      * that can be used in the description of
568      * a people tag.
569      *
570      * determined by $config['peopletag']['desclimit']
571      * if not set, falls back to $config['site']['textlimit']
572      *
573      * @return integer maximum number of characters
574      */
575
576     static function maxDescription()
577     {
578         $desclimit = common_config('peopletag', 'desclimit');
579         // null => use global limit (distinct from 0!)
580         if (is_null($desclimit)) {
581             $desclimit = common_config('site', 'textlimit');
582         }
583         return $desclimit;
584     }
585
586     /**
587      * check if the length of given text exceeds
588      * character limit.
589      *
590      * @param string $desc  the description
591      *
592      * @return boolean is the descripition too long?
593      */
594
595     static function descriptionTooLong($desc)
596     {
597         $desclimit = self::maxDescription();
598         return ($desclimit > 0 && !empty($desc) && (mb_strlen($desc) > $desclimit));
599     }
600
601     /**
602      * save a new people tag, this should be always used
603      * since it makes uri, homeurl, created and modified
604      * timestamps and performs checks.
605      *
606      * @param array $fields an array with fields and their values
607      *
608      * @return mixed Profile_list on success, false on fail
609      */
610     static function saveNew($fields) {
611         extract($fields);
612
613         $ptag = new Profile_list();
614
615         $ptag->query('BEGIN');
616
617         if (empty($tagger)) {
618             // TRANS: Server exception saving new tag without having a tagger specified.
619             throw new Exception(_('No tagger specified.'));
620         }
621
622         if (empty($tag)) {
623             // TRANS: Server exception saving new tag without having a tag specified.
624             throw new Exception(_('No tag specified.'));
625         }
626
627         if (empty($mainpage)) {
628             $mainpage = null;
629         }
630
631         if (empty($uri)) {
632             // fill in later...
633             $uri = null;
634         }
635
636         if (empty($mainpage)) {
637             $mainpage = null;
638         }
639
640         if (empty($description)) {
641             $description = null;
642         }
643
644         if (empty($private)) {
645             $private = false;
646         }
647
648         $ptag->tagger      = $tagger;
649         $ptag->tag         = $tag;
650         $ptag->description = $description;
651         $ptag->private     = $private;
652         $ptag->uri         = $uri;
653         $ptag->mainpage    = $mainpage;
654         $ptag->created     = common_sql_now();
655         $ptag->modified    = common_sql_now();
656
657         $result = $ptag->insert();
658
659         if (!$result) {
660             common_log_db_error($ptag, 'INSERT', __FILE__);
661             // TRANS: Server exception saving new tag.
662             throw new ServerException(_('Could not create profile tag.'));
663         }
664
665         if (!isset($uri) || empty($uri)) {
666             $orig = clone($ptag);
667             $ptag->uri = common_local_url('profiletagbyid', array('id' => $ptag->id, 'tagger_id' => $ptag->tagger));
668             $result = $ptag->update($orig);
669             if (!$result) {
670                 common_log_db_error($ptag, 'UPDATE', __FILE__);
671             // TRANS: Server exception saving new tag.
672                 throw new ServerException(_('Could not set profile tag URI.'));
673             }
674         }
675
676         if (!isset($mainpage) || empty($mainpage)) {
677             $orig = clone($ptag);
678             $user = User::staticGet('id', $ptag->tagger);
679             if(!empty($user)) {
680                 $ptag->mainpage = common_local_url('showprofiletag', array('tag' => $ptag->tag, 'tagger' => $user->nickname));
681             } else {
682                 $ptag->mainpage = $uri; // assume this is a remote peopletag and the uri works
683             }
684
685             $result = $ptag->update($orig);
686             if (!$result) {
687                 common_log_db_error($ptag, 'UPDATE', __FILE__);
688                 // TRANS: Server exception saving new tag.
689                 throw new ServerException(_('Could not set profile tag mainpage.'));
690             }
691         }
692         return $ptag;
693     }
694
695     /**
696      * get all items at given cursor position for api
697      *
698      * @param callback $fn  a function that takes the following arguments in order:
699      *                      $offset, $limit, $since_id, $max_id
700      *                      and returns a Profile_list object after making the DB query
701      * @param array $args   arguments required for $fn
702      * @param integer $cursor   the cursor
703      * @param integer $count    max. number of results
704      *
705      * Algorithm:
706      * - if cursor is 0, return empty list
707      * - if cursor is -1, get first 21 items, next_cursor = 20th prev_cursor = 0
708      * - if cursor is +ve get 22 consecutive items before starting at cursor
709      *   - return items[1..20] if items[0] == cursor else return items[0..21]
710      *   - prev_cursor = items[1]
711      *   - next_cursor = id of the last item being returned
712      *
713      * - if cursor is -ve get 22 consecutive items after cursor starting at cursor
714      *   - return items[1..20]
715      *
716      * @returns array (array (mixed items), int next_cursor, int previous_cursor)
717      */
718
719      // XXX: This should be in Memcached_DataObject... eventually.
720
721     static function getAtCursor($fn, $args, $cursor, $count=20)
722     {
723         $items = array();
724
725         $since_id = 0;
726         $max_id = 0;
727         $next_cursor = 0;
728         $prev_cursor = 0;
729
730         if($cursor > 0) {
731             // if cursor is +ve fetch $count+2 items before cursor starting at cursor
732             $max_id = $cursor;
733             $fn_args = array_merge($args, array(0, $count+2, 0, $max_id));
734             $list = call_user_func_array($fn, $fn_args);
735             while($list->fetch()) {
736                 $items[] = clone($list);
737             }
738
739             if ((isset($items[0]->cursor) && $items[0]->cursor == $cursor) ||
740                 $items[0]->id == $cursor) {
741                 array_shift($items);
742                 $prev_cursor = isset($items[0]->cursor) ?
743                     -$items[0]->cursor : -$items[0]->id;
744             } else {
745                 if (count($items) > $count+1) {
746                     array_shift($items);
747                 }
748                 // this means the cursor item has been deleted, check to see if there are more
749                 $fn_args = array_merge($args, array(0, 1, $cursor));
750                 $more = call_user_func($fn, $fn_args);
751                 if (!$more->fetch() || empty($more)) {
752                     // no more items.
753                     $prev_cursor = 0;
754                 } else {
755                     $prev_cursor = isset($items[0]->cursor) ?
756                         -$items[0]->cursor : -$items[0]->id;
757                 }
758             }
759
760             if (count($items)==$count+1) {
761                 // this means there is a next page.
762                 $next = array_pop($items);
763                 $next_cursor = isset($next->cursor) ?
764                     $items[$count-1]->cursor : $items[$count-1]->id;
765             }
766
767         } else if($cursor < -1) {
768             // if cursor is -ve fetch $count+2 items created after -$cursor-1
769             $cursor = abs($cursor);
770             $since_id = $cursor-1;
771
772             $fn_args = array_merge($args, array(0, $count+2, $since_id));
773             $list = call_user_func_array($fn, $fn_args);
774             while($list->fetch()) {
775                 $items[] = clone($list);
776             }
777
778             $end = count($items)-1;
779             if ((isset($items[$end]->cursor) && $items[$end]->cursor == $cursor) ||
780                 $items[$end]->id == $cursor) {
781                 array_pop($items);
782                 $next_cursor = isset($items[$end-1]->cursor) ?
783                     $items[$end-1]->cursor : $items[$end-1]->id;
784             } else {
785                 $next_cursor = isset($items[$end]->cursor) ?
786                     $items[$end]->cursor : $items[$end]->id;
787                 if ($end > $count) array_pop($items); // excess item.
788
789                 // check if there are more items for next page
790                 $fn_args = array_merge($args, array(0, 1, 0, $cursor));
791                 $more = call_user_func_array($fn, $fn_args);
792                 if (!$more->fetch() || empty($more)) {
793                     $next_cursor = 0;
794                 }
795             }
796
797             if (count($items) == $count+1) {
798                 // this means there is a previous page.
799                 $prev = array_shift($items);
800                 $prev_cursor = isset($prev->cursor) ?
801                     -$items[0]->cursor : -$items[0]->id;
802             }
803         } else if($cursor == -1) {
804             $fn_args = array_merge($args, array(0, $count+1));
805             $list = call_user_func_array($fn, $fn_args);
806
807             while($list->fetch()) {
808                 $items[] = clone($list);
809             }
810
811             if (count($items)==$count+1) {
812                 $next = array_pop($items);
813                 if(isset($next->cursor)) {
814                     $next_cursor = $items[$count-1]->cursor;
815                 } else {
816                     $next_cursor = $items[$count-1]->id;
817                 }
818             }
819
820         }
821         return array($items, $next_cursor, $prev_cursor);
822     }
823
824     /**
825      * save a collection of people tags into the cache
826      *
827      * @param string $ckey  cache key
828      * @param Profile_list &$tag the results to store
829      * @param integer $offset   offset for slicing results
830      * @param integer $limit    maximum number of results
831      *
832      * @return boolean success
833      */
834
835     static function setCache($ckey, &$tag, $offset=0, $limit=null) {
836         $cache = Cache::instance();
837         if (empty($cache)) {
838             return false;
839         }
840         $str = '';
841         $tags = array();
842         while ($tag->fetch()) {
843             $str .= $tag->tagger . ':' . $tag->tag . ';';
844             $tags[] = clone($tag);
845         }
846         $str = substr($str, 0, -1);
847         if ($offset>=0 && !is_null($limit)) {
848             $tags = array_slice($tags, $offset, $limit);
849         }
850
851         $tag = new ArrayWrapper($tags);
852
853         return self::cacheSet($ckey, $str);
854     }
855
856     /**
857      * get people tags from the cache
858      *
859      * @param string $ckey  cache key
860      * @param integer $offset   offset for slicing
861      * @param integer $limit    limit
862      *
863      * @return Profile_list results
864      */
865
866     static function getCached($ckey, $offset=0, $limit=null) {
867
868         $keys_str = self::cacheGet($ckey);
869         if ($keys_str === false) {
870             return false;
871         }
872
873         $pairs = explode(';', $keys_str);
874         $keys = array();
875         foreach ($pairs as $pair) {
876             $keys[] = explode(':', $pair);
877         }
878
879         if ($offset>=0 && !is_null($limit)) {
880             $keys = array_slice($keys, $offset, $limit);
881         }
882         return self::getByKeys($keys);
883     }
884
885     /**
886      * get Profile_list objects from the database
887      * given their (tag, tagger) key pairs.
888      *
889      * @param array $keys   array of array(tagger, tag)
890      *
891      * @return Profile_list results
892      */
893
894     static function getByKeys($keys) {
895         $cache = Cache::instance();
896
897         if (!empty($cache)) {
898             $tags = array();
899
900             foreach ($keys as $key) {
901                 $t = Profile_list::getByTaggerAndTag($key[0], $key[1]);
902                 if (!empty($t)) {
903                     $tags[] = $t;
904                 }
905             }
906             return new ArrayWrapper($tags);
907         } else {
908             $tag = new Profile_list();
909             if (empty($keys)) {
910                 //if no IDs requested, just return the tag object
911                 return $tag;
912             }
913
914             $pairs = array();
915             foreach ($keys as $key) {
916                 $pairs[] = '(' . $key[0] . ', "' . $key[1] . '")';
917             }
918
919             $tag->whereAdd('(tagger, tag) in (' . implode(', ', $pairs) . ')');
920
921             $tag->find();
922
923             $temp = array();
924
925             while ($tag->fetch()) {
926                 $temp[$tag->tagger.'-'.$tag->tag] = clone($tag);
927             }
928
929             $wrapped = array();
930
931             foreach ($keys as $key) {
932                 $id = $key[0].'-'.$key[1];
933                 if (array_key_exists($id, $temp)) {
934                     $wrapped[] = $temp[$id];
935                 }
936             }
937
938             return new ArrayWrapper($wrapped);
939         }
940     }
941 }