Development

/plugins/sfDoctrineActAsTaggablePlugin/trunk/lib/TaggableTemplate.class.php

You must first sign up to be able to contribute.

root/plugins/sfDoctrineActAsTaggablePlugin/trunk/lib/TaggableTemplate.class.php

Revision 27427, 19.9 kB (checked in by boutell, 1 week ago)

more changes from punkave

Line 
1 <?php
2 /*
3  * This file is part of the sfDoctrineActAsTaggablePlugin package.
4  * But is fully inspired by sfPropelActAsTaggableBehavior package.
5  *
6  * (c) 2007 Xavier Lacot <xavier@lacot.org> - sfPropelActAsTaggableBehavior
7  * (c) 2008 Mickael Kurmann <mickael.kurmann@gmail.com> - sfDoctrineActAsTaggablePlugin
8  *
9  * For the full copyright and license information, please view the LICENSE
10  * file that was distributed with this source code.
11  */
12
13 /*
14  * This behavior permits to attach tags to Doctrine objects. Some more bits about
15  * the philosophy of the stuff:
16  *
17  * - taggable objects must have a primary key
18  * - tags are saved when the object is saved, not before
19  *
20  * - one object cannot be tagged twice with the same tag. When trying to use
21  *   twice the same tag on one object, the second tagging will be ignored
22  *
23  * - the tags associated to one taggable object are only loaded when necessary.
24  *   Then they are cached.
25  *
26  * - once created, tags never change in the Tag table. When using replaceTag(),
27  *   a new tag is created if necessary, but the old one is not deleted.
28  *
29  *
30  * The plugin associates a parameterHolder to Propel objects, with 3 namespaces:
31  *
32  * - tags:
33  *     Tags that have been attached to the object, but not yet saved.
34  *     Contract: tags are disjoin of (saved_tags union removed_tags)
35  *
36  * - saved_tags:
37  *     Tags that are presently saved in the database
38  *
39  * - removed_tags:
40  *     Tags that are presently saved in the database, but which will be removed
41  *     at the next save()
42  *     Contract: removed_tags are disjoin of (tags union saved_tags)
43  *
44  *
45  * @author   Xavier Lacot <xavier@lacot.org>
46  * @see      http://www.symfony-project.com/trac/wiki/sfDoctrineActAsTaggablePlugin
47  */
48  
49  
50 class TaggableListener extends Doctrine_Record_Listener
51 {
52     /**
53     * Tags saving logic, runned after the object himself has been saved
54     *
55     * @param      Doctrine_Event  $event
56     */
57     public function postSave(Doctrine_Event $event)
58     {
59
60         $object = $event->getInvoker();
61         
62         $added_tags = Taggable::get_tags($object);
63         $removed_tags = array_keys(Taggable::get_removed_tags($object));
64         
65         // save new tags
66         foreach ($added_tags as $tagname)
67         {
68             $tag = PluginTagTable::findOrCreateByTagName($tagname);
69             $tag->save();
70             $tagging = new Tagging();
71             $tagging->tag_id = $tag->id;
72             $tagging->taggable_id = $object->id;
73             $tagging->taggable_model = get_class($object);
74             $tagging->save();
75         }
76         
77         if($removed_tags)
78         {
79             $q = Doctrine_Query::create()->select('t.id')
80                 ->from('Tag t INDEXBY t.id')
81                 ->whereIn('t.name', $removed_tags);
82                                 
83             $removed_tag_ids = array_keys($q->execute(array(), Doctrine::HYDRATE_ARRAY));
84             
85             Doctrine::getTable('Tagging')->createQuery()
86                 ->delete()
87                 ->whereIn('tag_id', $removed_tag_ids)
88                 ->addWhere('taggable_id = ?', $object->id)
89                 ->addWhere('taggable_model = ?', get_class($object))
90                 ->execute();
91         }
92
93         $tags = array_merge(Taggable::get_tags($object) , $object->getSavedTags());
94         
95         Taggable::set_saved_tags($object, $tags);
96         Taggable::clear_tags($object);
97         Taggable::clear_removed_tags($object);
98     }
99
100     /**
101     * Delete related Taggings when this object is deleted
102     *
103     * @param      Doctrine_Event $event
104     */
105     public function preDelete(Doctrine_Event $event)
106     {
107       
108         $object = $event->getInvoker();
109         
110         Doctrine::getTable('Tagging')->createQuery()
111           ->delete()
112           ->addWhere('taggable_id = ?', $object->id)
113           ->addWhere('taggable_model = ?', get_class($object))
114           ->execute();
115     }
116 }
117
118 class Taggable extends Doctrine_Template
119 {   
120     public function setTableDefinition()
121     {
122         $this->addListener(new TaggableListener());
123     }
124     
125     /**
126     * parameterHolder access methods
127     */
128     public static function getTagsHolder($object)
129     {
130         if ((!isset($object->_tags)) || ($object->_tags == null))
131         {
132             if (class_exists('sfNamespacedParameterHolder'))
133             {
134                 // Symfony 1.1
135                 $parameter_holder = 'sfNamespacedParameterHolder';
136             }
137             else
138             {
139                 // Symfony 1.0
140                 $parameter_holder = 'sfParameterHolder';
141             }
142
143             $object->mapValue('_tags', new $parameter_holder());
144         }
145
146         return $object->_tags;
147     }
148
149     public static function add_tag($object, $tag, $options = array())
150     {
151         $tag = TaggableToolkit::cleanTagName($tag, $options);
152
153         if (strlen($tag) > 0)
154         {
155             self::getTagsHolder($object)->set($tag, $tag, 'tags');
156         }
157     }
158
159     public static function clear_tags($object)
160     {
161         return self::getTagsHolder($object)->removeNamespace('tags');
162     }
163
164     public static function get_tags($object)
165     {
166         return self::getTagsHolder($object)->getAll('tags');
167     }
168
169     public static function set_tags($object, $tags)
170     {
171         self::clear_tags($object);
172         self::getTagsHolder($object)->add($tags, 'tags');
173     }
174
175     public static function add_saved_tag($object, $tag)
176     {
177         self::getTagsHolder($object)->set($tag, $tag, 'saved_tags');
178     }
179
180     public static function clear_saved_tags($object)
181     {
182         return self::getTagsHolder($object)->removeNamespace('saved_tags');
183     }
184
185     public static function get_saved_tags($object)
186     {
187         return self::getTagsHolder($object)->getAll('saved_tags');
188     }
189
190     public static function set_saved_tags($object, $tags = array())
191     {
192         self::clear_saved_tags($object);
193         self::getTagsHolder($object)->add($tags, 'saved_tags');
194     }
195
196     public static function add_removed_tag($object, $tag)
197     {
198         self::getTagsHolder($object)->set($tag, $tag, 'removed_tags');
199     }
200
201     public static function clear_removed_tags($object)
202     {
203         return self::getTagsHolder($object)->removeNamespace('removed_tags');
204     }
205
206     public static function get_removed_tags($object)
207     {
208         return self::getTagsHolder($object)->getAll('removed_tags');
209     }
210
211     public static function set_removed_tags($object, $tags)
212     {
213         self::clear_removed_tags($object);
214         self::getTagsHolder($object)->add($tags, 'removed_tags');
215     }
216     
217     /**
218     * Adds a tag to the object. The "tagname" param can be a string or an array
219     * of strings. These 3 code sequences produce an equivalent result :
220     *
221     * 1- $object->addTag('tag1,tag2,tag3');
222     * 2- $object->addTag('tag1');
223     *    $object->addTag('tag2');
224     *    $object->addTag('tag3');
225     * 3- $object->addTag(array('tag1','tag2','tag3'));
226     *
227     * @param      mixed       $tagname
228     */
229     public function addTag($tagname, $options = array())
230     {
231         $tagname = TaggableToolkit::explodeTagString($tagname);
232
233         if (is_array($tagname))
234         {
235             foreach ($tagname as $tag)
236             {
237                 $this->addTag($tag, $options);
238             }
239         }
240         else
241         {
242             $removed_tags = $this->get_removed_tags($this->getInvoker()) ;
243
244             if (isset($removed_tags[$tagname]))
245             {
246                 unset($removed_tags[$tagname]);
247                 $this->set_removed_tags($this->getInvoker(), $removed_tags);
248                 $this->add_saved_tag($this->getInvoker(), $tagname);
249             }
250             else
251             {
252                 $saved_tags = $this->getSavedTags();
253
254                 if (sfConfig::get('app_sfDoctrineActAsTaggablePlugin_triple_distinct', false))
255                 {
256                     // the binome namespace:key must be unique
257                     $triple = TaggableToolkit::extractTriple($tagname);
258                     
259                     if (!is_null($triple[1]) && !is_null($triple[2]))
260                     {                       
261                         $tags = $this->getTags(array('triple' => true, 'return' => 'tag'));
262                         
263                         $pattern = '/^'.$triple[1].':'.$triple[2].'=(.*)$/';
264                         
265                         $removed = array();
266                     
267                         foreach ($tags as $tag)
268                         {
269                             if (preg_match($pattern, $tag))
270                             {
271                               $removed[] = $tag;
272                             }
273                         }
274                     
275                         $this->removeTag($removed);
276                     }
277                 }
278                 
279                 if (!isset($saved_tags[$tagname]))
280                 {
281                     $this->add_tag($this->getInvoker(), $tagname, $options);
282                 }
283             }
284         }
285     }
286
287     /**
288     * Retrieves from the database tags that have been atached to the object.
289     * Once loaded, this saved tags list is cached and updated in memory.
290     */
291     public function getSavedTags()
292     {
293         $option = $this->getTagsHolder($this->getInvoker());
294         
295         if (!isset($option) || !$option->hasNamespace('saved_tags'))
296         {
297             // if record is new
298             if ($this->getInvoker()->state() === Doctrine_Record::STATE_TCLEAN)
299             {
300                 $this->set_saved_tags($this->getInvoker(), array());
301                 return array();
302             }
303             else
304             {
305                 $q = Doctrine_Query::create()
306                   ->select('t.name')
307                   ->from('Tag t INDEXBY t.name, t.Tagging tg')
308                   ->where('tg.taggable_id = ?', $this->getInvoker()->id)
309                   ->addWhere('tg.taggable_model = ?', get_class($this->getInvoker()))
310                 ;
311
312                 $saved_tags = $q->execute(array(), Doctrine::HYDRATE_ARRAY);
313                 $tags = array();
314                 
315                 foreach ($saved_tags as $key => $infos)
316                 {
317                     $tags[$key] = $key;
318                 }
319                 
320                 $this->set_saved_tags($this->getInvoker(), $tags);
321                 
322                 return $tags;
323             }
324         }
325         else
326         {
327             return $this->get_saved_tags($this->getInvoker()) ;
328         }
329     }
330
331     /**
332     * Returns the list of the tags attached to the object, whatever they have
333     * already been saved or not.
334     *
335     * @param       $object
336     */
337     public function getTags($options = array())
338     {
339         $tags = array_merge($this->get_tags($this->getInvoker()) , $this->getSavedTags());
340         
341         if (isset($options['is_triple']) && (true === $options['is_triple']))
342         {
343             $tags = array_map(array('TaggableToolkit', 'extractTriple'), $tags);
344             $pattern = array('tag', 'namespace', 'key', 'value');
345             
346             foreach ($pattern as $key => $value)
347             {
348                 if (isset($options[$value]))
349                 {
350                     $tags_array = array();
351                     
352                     foreach ($tags as $tag)
353                     {
354                         if ($tag[$key] == $options[$value])
355                         {
356                             $tags_array[] = $tag;
357                         }
358                     }
359                     
360                     $tags = $tags_array;
361                 }
362             }
363             
364             $return = (isset($options['return']) && in_array($options['return'], $pattern)) ? $options['return'] : 'all';
365             
366             if ('all' != $return)
367             {
368                 $keys = array_flip($pattern);
369                 $tags_array = array();
370                 
371                 foreach ($tags as $tag)
372                 {
373                     if (null != $tag[$keys[$return]])
374                     {
375                         $tags_array[] = $tag[$keys[$return]];
376                     }
377                 }
378                 
379                 $tags = array_unique($tags_array);
380             }
381         }
382
383         if (!isset($return) || ('all' != $return))
384         {
385             ksort($tags);
386             
387             if (isset($options['serialized']) && (true === $options['serialized']))
388             {
389                 $tags = implode(', ', $tags);
390             }
391         }
392
393         return $tags;
394     }
395
396     /**
397     * Returns true if the object has a tag. If a tag ar an array of tags is
398     * passed in second parameter, checks if these tags are attached to the object
399     *
400     * These 3 calls are equivalent :
401     * 1- $object->hasTag('tag1')
402     *    && $object->hasTag('tag2')
403     *    && $object->hasTag('tag3');
404     * 2- $object->hasTag('tag1,tag2,tag3');
405     * 3- $object->hasTag(array('tag1', 'tag2', 'tag3'));
406     *
407     * @param      mixed       $tag
408     */
409     public function hasTag($tag = null)
410     {
411         $tag = TaggableToolkit::explodeTagString($tag);
412
413         if (is_array($tag))
414         {
415             $result = true;
416         
417             foreach ($tag as $tagname)
418             {
419                 $result = $result && $this->hasTag($tagname);
420             }
421         
422             return $result;
423         }
424         else
425         {
426             $tags = $this->get_tags($this->getInvoker()) ;
427
428             if ($tag === null)
429             {
430                 return (count($tags) > 0) || (count($this->getSavedTags()) > 0);
431             }
432             elseif (is_string($tag))
433             {
434                 $tag = TaggableToolkit::cleanTagName($tag);
435                 
436                 if (isset($tags[$tag]))
437                 {
438                     return true;
439                 }
440                 else
441                 {
442                     $saved_tags = $this->getSavedTags();
443                     $removed_tags = $this->get_removed_tags($this->getInvoker()) ;
444                     return isset($saved_tags[$tag]) && !isset($removed_tags[$tag]);
445                 }
446             }
447             else
448             {
449                 $msg = sprintf('hasTag() does not support this type of argument : %s.', get_class($tag));
450                 throw new Exception($msg);
451             }
452         }
453     }
454
455     /**
456     * Preload tags for a set of objects. It might be usefull in case you want to
457     * display a long list of taggable objects with their associated tags: it
458     * avoids to load tags per object, and gets all tags in a few requests.
459     *
460     * @param      array       $objects
461     */
462     public static function preloadTags(&$objects)
463     {   
464         // FIXME: usage of group_concat... mysql specific
465         return array();
466         // $searched = array();
467         //
468         //         foreach ($objects as $object)
469         //         {
470         //             $class = get_class($object);
471         //             
472         //             if (!isset($searched[$class]))
473         //             {
474         //                 $searched[$class] = array();
475         //             }
476         //             
477         //             $searched[$class][$object->getPrimaryKey()] = $object;
478         //         }
479         //
480         //         if (count($searched) > 0)
481         //         {
482         //             $con = Propel::getConnection();
483         //             
484         //             foreach ($searched as $model => $instances)
485         //             {
486         //                 Doctrine_Query::create()
487         //                               ->select('t.taggable_id')
488         //                               ->from('Tagging t')
489         //                 array_map(array('sfDoctrineActAsTaggable', 'set_saved_tags'),
490         //                           $instances,
491         //                           array_fill(0, count($instances), array()));
492         //                 $keys = array_keys($instances);
493         //                 
494         //                 $query = 'SELECT %s as id,
495         //                                  GROUP_CONCAT(%s) as tags
496         //                           FROM %s, %s
497         //                           WHERE %s IN (%s)
498         //                           AND %s=?
499         //                           AND %s=%s
500         //                           GROUP BY %s';
501         //                 
502         //                 $query = sprintf($query,
503         //                                  TaggingPeer::TAGGABLE_ID,
504         //                                  TagPeer::NAME,
505         //                                  TaggingPeer::TABLE_NAME,
506         //                                  TagPeer::TABLE_NAME,
507         //                                  TaggingPeer::TAGGABLE_ID,
508         //                                  implode($keys, ','),
509         //                                  TaggingPeer::TAGGABLE_MODEL,
510         //                                  TaggingPeer::TAG_ID,
511         //                                  TagPeer::ID,
512         //                                  TaggingPeer::TAGGABLE_ID);
513         //                 $stmt = $con->prepareStatement($query);
514         //                 $stmt->setString(1, $model);
515         //                 $rs = $stmt->executeQuery();
516         //                 
517         //                 while ($rs->next())
518         //                 {
519         //                     $object = $instances[$rs->getInt('id')];
520         //                     $object_tags = explode(',', $rs->getString('tags'));
521         //                     $tags = array();
522         //                     
523         //                     foreach ($object_tags as $tag)
524         //                     {
525         //                         $tags[$tag] = $tag;
526         //                     }
527         //                     
528         //                     self::set_saved_tags($this->getInvoker(), $object, $tags);
529         //                 }
530         //             }
531         //         }
532     }
533
534     /**
535     * Removes all the tags associated to the object.
536     *
537     * @param       $object
538     */
539     public function removeAllTags()
540     {
541         $saved_tags = $this->getSavedTags();
542         
543         $this->set_saved_tags($this->getInvoker(), array());
544         $this->set_tags($this->getInvoker(), array());       
545         $this->set_removed_tags($this->getInvoker(), array_merge($this->get_removed_tags($this->getInvoker()) , $saved_tags));
546     }
547
548     /**
549     * Removes a tag or a set of tags from the object. The
550     * parameter might be an array of tags or a comma-separated string.
551     *
552     * @param      mixed       $tagname
553     */
554     public function removeTag($tagname)
555     {
556         $tagname = TaggableToolkit::explodeTagString($tagname);
557         
558         if (is_array($tagname))
559         {
560             foreach ($tagname as $tag)
561             {
562                 $this->removeTag($tag);
563             }
564         }
565         else
566         {
567             $tagname = TaggableToolkit::cleanTagName($tagname);
568             
569             $tags = $this->get_tags($this->getInvoker()) ;
570             $saved_tags = $this->getSavedTags();
571         
572             if (isset($tags[$tagname]))
573             {
574               unset($tags[$tagname]);
575               $this->set_tags($this->getInvoker(), $tags);
576             }
577         
578             if (isset($saved_tags[$tagname]))
579             {
580                 unset($saved_tags[$tagname]);
581                 $this->set_saved_tags($this->getInvoker(), $saved_tags);
582                 $this->add_removed_tag($this->getInvoker(), $tagname);
583             }
584         }
585     }
586
587     /**
588     * Replaces a tag with an other one. If the third optionnal parameter is not
589     * passed, the second tag will simply be removed
590     *
591     * @param       $object
592     * @param      String      $tagname
593     * @param      String      $replacement
594     */
595     public function replaceTag($tagname, $replacement = null)
596     {
597         if (($replacement != $tagname) && ($tagname != null))
598         {
599             $this->removeTag($tagname);
600             
601             if ($replacement != null)
602             {
603                 $this->addTag($replacement);
604             }
605         }
606     }
607
608     /**
609     * Sets the tags of an object. As usual, the second parameter might be an
610     * array of tags or a comma-separated string.
611     *
612     * @param       $object
613     * @param      mixed       $tagname
614     */
615     public function setTags($tagname)
616     {
617         $this->removeAllTags();
618         $this->addTag($tagname);
619     }
620 }
Note: See TracBrowser for help on using the browser.

The Sensio Labs Network

Since 1998, Sensio Labs has been promoting the Open-Source software movement by providing quality web application development, training, consulting.
Sensio Labs also supports several large Open-Source projects.