sfTagtoolsPlugin
REQUIREMENTS: READ THIS FIRST
You MUST have the sfPropelActAsTaggablePlugin as well. This plugin
provides additional services to applications that utilize that plugin. There is
no point in using this plugin without it.
To use the admin page and typeahead features, you must also have Prototype (prototype.js)
loading in your pages.
Introduction
This plugin provides many additional services above and beyond those
standard in the excellent sfPropelActAsTaggablePlugin. This plugin
is less mature than sfPropelActAsTaggablePlugin but provides quite a bit
of user interface "glue" to flesh it out.
Contact
sfTagtoolsPlugin was created by the team at [http://www.punkave.com/ P'unk Avenue].
Please address questions, concerns and patches to [mailto:tom@punkave.com Tom Boutell].
Features
Typeahead for tags, with autocomplete on tab or mouseclick. Similar to Flickr's solution, which is familiar to many users.
See: lib/helper/tagaheadHelper.php
The concept of featured tags. These are just a handy way to mark certain
tags as "featured" for your own UI's purposes. If a tag has a
corresponding !FeatureTag object, you know it's a featured tag.
We provide this so that you can mark tags as "featured" in...
A nicely AJAXified admin page for renaming, merging, deleting,
prioritizing and featuring tags. Note that you don't have to
use !FeaturedTag to get this admin page, that's an optional feature.
The concept of prioritized tags, implemented by the
!PriorityTag class. A tag with a higher priority appears first in results
returned by sfTagtoolsToolkit::getBeginningWith(). Again, this is
an optional feature, you can use the typeahead and the admin page
without !PriorityTag. "But what is sfTagtoolsToolkit::getBeginningWith()?" you ask. This leads
us to:
A way to retrieve tags ranked in order by popularity, optionally
requiring that they begin with a particular string. See
sfTagtoolsToolkit::getBeginningWith(). This method began life as
part of the typeahead mechanism but with a blank initial string
it's quite useful for creating tag browsers, too.
A way to fetch objects of a particular taggable model class
which are related to a particular set of tags, ranking them in
descending order by the number of related tags, with high priority
tags receiving an additional boost.
Philosophy
sfPropelActAsTaggablePlugin works its magic by storing the name of the
tagged object class as a string in the database (taggable_model). This
is great in a lot of ways but it causes problems when we want to
rank things meaningfully so that stale taggings pointing to dead objects
are not considered. This can be worked around to an extent using
SQL's provision for an "IN" clause (exposed by the retrieveByPKs
method of peer classes in Propel), and Xavier Lacot's code makes
heroic use of this feature. However this tends to fall apart if you want
to, let's say, quickly count the tagged objects of a particular model
that still exist without the overhead of hydrating them all (for
instance, to build a tag admin page). It also falls apart if you have
hundreds of thousands of records, creating IN clauses that are too large for
a MySQL packet (and inefficient in any case). It's true that an id is a whole lot smaller than an actual object, but when you have enough of them it's still bad.
So if you want to count the tagged objects of a particular model that still
exist and meet a particular criterion of "live-ness" or "published-ness"
without hydrating them all... welcome to custom SQL query land!
Happily I have coded those queries for you.
So although it can be used without, sfTagtoolsPlugin works best when you
describe your taggable classes to it via app.yml. This allows
sfTagtoolsPlugin to work around this design limitation of
sfPropelActAsTaggablePlugin by joining with the tables that contain
the taggable classes when making custom queries to efficiently
fetch just the information needed.
"Isn't that still slow?" There is a small performance hit relative to
selecting against the tags and taggings alone, but it's
not significant in "big-O notation" terms (at least, not if MySQL does
its job and optimizes things properly). Still, if you have a
zillion taggable classes, you might want to go with a single
taggable class instead and reference it from tables for each of the
"real" objects. Sigh... wouldn't it be nice if Propel truly
understood multiple inheritance?
Installation
Just install the plugin. Okay, it's not quite that simple.
You'll need to enable the tagtools module in your application. In my
apps/frontend/config/settings.yml, I have:
# Activated modules from plugins or from the symfony core
enabled_modules: [default, sfGuardAuth, sfGuardUser, sfGuardGroup, sfGuardPermission, tagtools]
Your list of modules will of course vary, just be sure to add tagtools to the settings you have for enabled_modules.
You will also need to enable prototype in your pages. In the default section of apps/frontend/config/view.yml, you can use:
javascripts: [%SF_PROTOTYPE_WEB_DIR%/js/prototype]
To use our typeahead support, you will also need tagahead.js:
javascripts: [%SF_PROTOTYPE_WEB_DIR%/js/prototype, %SF_WEB_ROOT%/tagtools/js/tagahead.js]
Your site may use other JavaScript libraries which can be loaded in a comma-separated list.
Finally, to make the typeahead ("tagahead") support work properly, you must have at least a few bare-bones CSS settings to correctly display the suggested tags. See sfTagtoolsPlugin/web/css/suggested.css for details.
That's it for essential configuration. But to take full advantage, you should also describe your taggable classes
in app.yml. And you may wish to
enable the optional !FeaturedTag and !PriorityTag support although this plugin works quite well without those.
Here's an example:
all:
sfTagtoolsPlugin:
featured: true
priority: true
taggables:
- name: Release
label: Releases
cssclass: releases
- name: PressKit
label: Presskits
cssclass: presskits
- name: Media
label: Media
cssclass: media
criteria_live: "TAGGABLE.live = true"
criteria_public: "TAGGABLE.published = true"
"What are criteria_live and criteria_public?" These criteria are
applied when deciding if the object a tagging refers to is
good enough to qualify as a "live" object. If your application, like
many of ours, involves objects that users abandon halfway through the
creation process, then you know exactly what I'm talking about.
criteria_public is similar, but is applied only if the user
is not logged in (isAuthenticated fails on the current sfUser).
The word TAGGABLE is automatically replaced with the name of the
table for the appropriate object class when the query is run.
"What are label and cssclass?" These are used in the admin page, so
they are optional if you don't use the admin page. You do need
the name field, which must be the actual PHP class name
(not table name) for that type of object.
"Can I use typeahead and the admin page without any of these
settings?" Yes, but sfTagtools won't be able to distinguish
stale taggings pointing to long-deleted objects from taggings
that point to live objects. So I strongly recommend that you
at least define your taggables:
sfTagtoolsPlugin:
taggables:
- name: Release
label: Releases
cssclass: releases
- name: PressKit
label: Presskits
cssclass: presskits
- name: Media
label: Media
cssclass: media
Otherwise you'll see typeahead suggestions for tags that are
no longer actually in use anywhere, perhaps for a good reason.
And you'll also see an inflated count of total occurrences for
each tag in the tagadmin module, because taggings are never
deleted and sfTagtools doesn't have enough information to join
with the taggables and count only the taggings that still matter.
Don't forget to "symfony cc"!
Implementing Typeahead
To implement the typeahead feature, use code like the following in your templates:
<?php use_helper("tagahead") ?>
<div class="form-row">
<label for="tags">Tags</label>
<?php echo tagahead_input_tag("tags", $tags) ?>
</div>
<div class="form-row">
<label for="tags-suggestions">Suggested Tags</label>
<div id="tags-suggestions"></div>
</div>
<?php echo tagahead_observer('tags') ?>
The tagahead_input_tag helper outputs an input tag with the specified name (which will also be its ID). The tagahead_observer helper outputs JavaScript code to dynamically update the tag suggestions based on what the user has typed so far.
Implementing Tag Admin
The tag admin page was activated when you enabled the tags module. It was also secured with a rule in sfTagtoolsPlugin/modules/tagtools/config/security.yml. However this rule simply prevents users from accessing the admin page if they are not authenticated. If your site has multiple levels of user privileges, you will want to adjust these settings so that only admins can delete, rename and merge tags!
To access the tag admin page, just visit:
!http://yoursite/tagtools/admin
Again, you must be authenticated first if you are using our provided security.yml rule.
version 0.33 - 2008-08-05
getBeginningWith: when joining with multiple taggables we do separate queries
consecutively and merge the results in order to avoid serious performance
issues when the database fails to spot potential optimizations and actually
examines a * b * c * d * e rows, where a * b * c * d * e tends to be a
rather large number.
An empty taggables array is now treated the same as no taggables setting
at all.
Further markdown corrections for the wiki.
version 0.32 - 2008-08-05
Another try at markdown format for documentation compatibility
with the wiki.
version 0.31 - 2008-08-05
README code examples now in markdown format for compatibility
with the new Symfony plugins wiki.
version 0.3 - 2008-07-31
package.xml added. It's a proper package now. Recreated plugin in the new
Symfony web site plugin CMS.
version 0.2 - 2008-07-30
The model classes were missing! To make matters worse, config/schema.yml
didn't have a package attribute, so they were rebuilt in the main app
folder. And in any case my custom methods were missing. Fixed. Thanks to
Xavier Lacot for pointing out the problem.
version 0.1 - 2008-07-28
Initial public release. Thanks to Xavier Lacot for
wiki:sfPropelActAsTaggableBehaviorPlugin, which makes this plugin possible.