/lib/Gedmo/Sluggable/SluggableListener.php
PHP | 376 lines | 215 code | 27 blank | 134 comment | 34 complexity | fe93afed2c0e2929fc9e07eb7b90b71c MD5 | raw file
- <?php
- namespace Gedmo\Sluggable;
- use Doctrine\Common\EventArgs;
- use Gedmo\Mapping\MappedEventSubscriber;
- use Gedmo\Sluggable\Mapping\Event\SluggableAdapter;
- use Doctrine\Common\Persistence\ObjectManager;
- /**
- * The SluggableListener handles the generation of slugs
- * for documents and entities.
- *
- * This behavior can inpact the performance of your application
- * since it does some additional calculations on persisted objects.
- *
- * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
- * @author Klein Florian <florian.klein@free.fr>
- * @subpackage SluggableListener
- * @package Gedmo.Sluggable
- * @link http://www.gediminasm.org
- * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
- */
- class SluggableListener extends MappedEventSubscriber
- {
- /**
- * The power exponent to jump
- * the slug unique number by tens.
- *
- * @var integer
- */
- private $exponent = 0;
- /**
- * Transliteration callback for slugs
- *
- * @var array
- */
- private $transliterator = array('Gedmo\Sluggable\Util\Urlizer', 'transliterate');
- /**
- * List of inserted slugs for each object class.
- * This is needed in case there are identical slug
- * composition in number of persisted objects
- *
- * @var array
- */
- private $persistedSlugs = array();
- /**
- * List of initialized slug handlers
- *
- * @var array
- */
- private $handlers = array();
- /**
- * Specifies the list of events to listen
- *
- * @return array
- */
- public function getSubscribedEvents()
- {
- return array(
- 'onFlush',
- 'loadClassMetadata'
- );
- }
- /**
- * Set the transliteration callable method
- * to transliterate slugs
- *
- * @param mixed $callable
- */
- public function setTransliterator($callable)
- {
- if (!is_callable($callable)) {
- throw new \Gedmo\Exception\InvalidArgumentException('Invalid transliterator callable parameter given');
- }
- $this->transliterator = $callable;
- }
- /**
- * Get currently used transliterator callable
- *
- * @return callable
- */
- public function getTransliterator()
- {
- return $this->transliterator;
- }
- /**
- * Mapps additional metadata
- *
- * @param EventArgs $eventArgs
- * @return void
- */
- public function loadClassMetadata(EventArgs $eventArgs)
- {
- $ea = $this->getEventAdapter($eventArgs);
- $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata());
- }
- /**
- * Generate slug on objects being updated during flush
- * if they require changing
- *
- * @param EventArgs $args
- * @return void
- */
- public function onFlush(EventArgs $args)
- {
- $ea = $this->getEventAdapter($args);
- $om = $ea->getObjectManager();
- $uow = $om->getUnitOfWork();
- // process all objects being inserted, using scheduled insertions instead
- // of prePersist in case if record will be changed before flushing this will
- // ensure correct result. No additional overhead is encoutered
- foreach ($ea->getScheduledObjectInsertions($uow) as $object) {
- $meta = $om->getClassMetadata(get_class($object));
- if ($config = $this->getConfiguration($om, $meta->name)) {
- // generate first to exclude this object from similar persisted slugs result
- $this->generateSlug($ea, $object);
- foreach ($config['fields'] as $slugField => $fieldsForSlugField) {
- $slug = $meta->getReflectionProperty($slugField)->getValue($object);
- $this->persistedSlugs[$config['useObjectClass']][$slugField][] = $slug;
- }
- }
- }
- // we use onFlush and not preUpdate event to let other
- // event listeners be nested together
- foreach ($ea->getScheduledObjectUpdates($uow) as $object) {
- $meta = $om->getClassMetadata(get_class($object));
- if ($config = $this->getConfiguration($om, $meta->name)) {
- foreach ($config['slugFields'] as $slugField) {
- if ($slugField['updatable']) {
- $this->generateSlug($ea, $object);
- }
- }
- }
- }
- }
- /**
- * {@inheritDoc}
- */
- protected function getNamespace()
- {
- return __NAMESPACE__;
- }
- /**
- * Get the slug handler instance by $class name
- *
- * @param string $class
- * @return Gedmo\Sluggable\Handler\SlugHandlerInterface
- */
- private function getHandler($class)
- {
- if (!isset($this->handlers[$class])) {
- $this->handlers[$class] = new $class($this);
- }
- return $this->handlers[$class];
- }
- /**
- * Creates the slug for object being flushed
- *
- * @param SluggableAdapter $ea
- * @param object $object
- * @throws UnexpectedValueException - if parameters are missing
- * or invalid
- * @return void
- */
- private function generateSlug(SluggableAdapter $ea, $object)
- {
- $om = $ea->getObjectManager();
- $meta = $om->getClassMetadata(get_class($object));
- $uow = $om->getUnitOfWork();
- $changeSet = $ea->getObjectChangeSet($uow, $object);
- $config = $this->getConfiguration($om, $meta->name);
- foreach ($config['fields'] as $slugField => $fieldsForSlugField) {
- // sort sluggable fields by position
- $fields = $fieldsForSlugField;
- usort($fields, function($a, $b) {
- if ($a['position'] == $b['position']) {
- return 1;
- }
- return ($a['position'] < $b['position']) ? -1 : 1;
- });
- $slugFieldConfig = $config['slugFields'][$slugField];
- // collect the slug from fields
- $slug = '';
- $needToChangeSlug = false;
- foreach ($fields as $sluggableField) {
- if (isset($changeSet[$sluggableField['field']])) {
- $needToChangeSlug = true;
- }
- $slug .= $meta->getReflectionProperty($sluggableField['field'])->getValue($object) . ' ';
- }
- // notify slug handlers --> onChangeDecision
- if (isset($config['handlers'])) {
- foreach ($config['handlers'] as $class => $options) {
- $this
- ->getHandler($class)
- ->onChangeDecision($ea, $slugFieldConfig, $object, $slug, $needToChangeSlug)
- ;
- }
- }
- // if slug is changed, do further processing
- if ($needToChangeSlug) {
- $mapping = $meta->getFieldMapping($slugFieldConfig['slug']);
- if (!strlen(trim($slug)) && (!isset($mapping['nullable']) || !$mapping['nullable'])) {
- throw new \Gedmo\Exception\UnexpectedValueException("Unable to find any non empty sluggable fields for slug [{$slugField}] , make sure they have something at least.");
- }
- // notify slug handlers --> postSlugBuild
- if (isset($config['handlers'])) {
- foreach ($config['handlers'] as $class => $options) {
- $this
- ->getHandler($class)
- ->postSlugBuild($ea, $slugFieldConfig, $object, $slug)
- ;
- }
- }
- // build the slug
- $slug = call_user_func_array(
- $this->transliterator,
- array($slug, $slugFieldConfig['separator'], $object)
- );
- // stylize the slug
- switch ($slugFieldConfig['style']) {
- case 'camel':
- $slug = preg_replace_callback(
- '@^[a-z]|' . $slugFieldConfig['separator'] . '[a-z]@smi',
- create_function('$m', 'return strtoupper($m[0]);'),
- $slug
- );
- break;
- default:
- // leave it as is
- break;
- }
- // cut slug if exceeded in length
- if (isset($mapping['length']) && strlen($slug) > $mapping['length']) {
- $slug = substr($slug, 0, $mapping['length']);
- }
- if (isset($mapping['nullable']) && $mapping['nullable'] && !$slug) {
- $slug = null;
- }
- // make unique slug if requested
- if ($slugFieldConfig['unique'] && !is_null($slug)) {
- $this->exponent = 0;
- $arrayConfig = $slugFieldConfig;
- $arrayConfig['useObjectClass'] = $config['useObjectClass'];
- $slug = $this->makeUniqueSlug($ea, $object, $slug, false, $arrayConfig);
- }
- // notify slug handlers --> onSlugCompletion
- if (isset($config['handlers'])) {
- foreach ($config['handlers'] as $class => $options) {
- $this
- ->getHandler($class)
- ->onSlugCompletion($ea, $slugFieldConfig, $object, $slug)
- ;
- }
- }
- // set the final slug
- $meta->getReflectionProperty($slugFieldConfig['slug'])->setValue($object, $slug);
- // recompute changeset
- $ea->recomputeSingleObjectChangeSet($uow, $meta, $object);
- }
- }
- }
- /**
- * Generates the unique slug
- *
- * @param SluggableAdapter $ea
- * @param object $object
- * @param string $preferedSlug
- * @param array $config[$slugField]
- * @return string - unique slug
- */
- private function makeUniqueSlug(SluggableAdapter $ea, $object, $preferedSlug, $recursing = false, $config = array())
- {
- $om = $ea->getObjectManager();
- $meta = $om->getClassMetadata(get_class($object));
- if (count ($config) == 0)
- {
- $config = $this->getConfiguration($om, $meta->name);
- }
- // search for similar slug
- $result = $ea->getSimilarSlugs($object, $meta, $config, $preferedSlug);
- // add similar persisted slugs into account
- $result += $this->getSimilarPersistedSlugs($config['useObjectClass'], $preferedSlug, $config['slug']);
- // leave only right slugs
- if (!$recursing) {
- $this->filterSimilarSlugs($result, $config, $preferedSlug);
- }
- if ($result) {
- $generatedSlug = $preferedSlug;
- $sameSlugs = array();
- foreach ((array)$result as $list) {
- $sameSlugs[] = $list[$config['slug']];
- }
- $i = pow(10, $this->exponent);
- do {
- $generatedSlug = $preferedSlug . $config['separator'] . $i++;
- } while (in_array($generatedSlug, $sameSlugs));
- $mapping = $meta->getFieldMapping($config['slug']);
- if (isset($mapping['length']) && strlen($generatedSlug) > $mapping['length']) {
- $generatedSlug = substr(
- $generatedSlug,
- 0,
- $mapping['length'] - (strlen($i) + strlen($config['separator']))
- );
- $this->exponent = strlen($i) - 1;
- $generatedSlug = $this->makeUniqueSlug($ea, $object, $generatedSlug, true, $config);
- }
- $preferedSlug = $generatedSlug;
- }
- return $preferedSlug;
- }
- /**
- * In case if any number of records are persisted instantly
- * and they contain same slugs. This method will filter those
- * identical slugs specialy for persisted objects. Returns
- * array of similar slugs found
- *
- * @param string $class
- * @param string $preferedSlug
- * @param string $slugField
- * @return array
- */
- private function getSimilarPersistedSlugs($class, $preferedSlug, $slugField)
- {
- $result = array();
- if (isset($this->persistedSlugs[$class][$slugField])) {
- array_walk($this->persistedSlugs[$class][$slugField], function($val) use ($preferedSlug, &$result, $slugField) {
- if (preg_match("@^{$preferedSlug}.*@smi", $val)) {
- $result[] = array($slugField => $val);
- }
- });
- }
- return $result;
- }
- /**
- * Filters $slugs which are matched as prefix but are
- * simply shorter slugs
- *
- * @param array $slugs
- * @param array $config
- * @param string $prefered
- */
- private function filterSimilarSlugs(array &$slugs, array &$config, $prefered)
- {
- foreach ($slugs as $key => $similar) {
- if (!preg_match("@{$prefered}($|{$config['separator']}[\d]+$)@smi", $similar[$config['slug']])) {
- unset($slugs[$key]);
- }
- }
- }
- }