PageRenderTime 27ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Gedmo/Sluggable/SluggableListener.php

https://github.com/kaiwa/DoctrineExtensions
PHP | 376 lines | 215 code | 27 blank | 134 comment | 34 complexity | fe93afed2c0e2929fc9e07eb7b90b71c MD5 | raw file
  1. <?php
  2. namespace Gedmo\Sluggable;
  3. use Doctrine\Common\EventArgs;
  4. use Gedmo\Mapping\MappedEventSubscriber;
  5. use Gedmo\Sluggable\Mapping\Event\SluggableAdapter;
  6. use Doctrine\Common\Persistence\ObjectManager;
  7. /**
  8. * The SluggableListener handles the generation of slugs
  9. * for documents and entities.
  10. *
  11. * This behavior can inpact the performance of your application
  12. * since it does some additional calculations on persisted objects.
  13. *
  14. * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
  15. * @author Klein Florian <florian.klein@free.fr>
  16. * @subpackage SluggableListener
  17. * @package Gedmo.Sluggable
  18. * @link http://www.gediminasm.org
  19. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  20. */
  21. class SluggableListener extends MappedEventSubscriber
  22. {
  23. /**
  24. * The power exponent to jump
  25. * the slug unique number by tens.
  26. *
  27. * @var integer
  28. */
  29. private $exponent = 0;
  30. /**
  31. * Transliteration callback for slugs
  32. *
  33. * @var array
  34. */
  35. private $transliterator = array('Gedmo\Sluggable\Util\Urlizer', 'transliterate');
  36. /**
  37. * List of inserted slugs for each object class.
  38. * This is needed in case there are identical slug
  39. * composition in number of persisted objects
  40. *
  41. * @var array
  42. */
  43. private $persistedSlugs = array();
  44. /**
  45. * List of initialized slug handlers
  46. *
  47. * @var array
  48. */
  49. private $handlers = array();
  50. /**
  51. * Specifies the list of events to listen
  52. *
  53. * @return array
  54. */
  55. public function getSubscribedEvents()
  56. {
  57. return array(
  58. 'onFlush',
  59. 'loadClassMetadata'
  60. );
  61. }
  62. /**
  63. * Set the transliteration callable method
  64. * to transliterate slugs
  65. *
  66. * @param mixed $callable
  67. */
  68. public function setTransliterator($callable)
  69. {
  70. if (!is_callable($callable)) {
  71. throw new \Gedmo\Exception\InvalidArgumentException('Invalid transliterator callable parameter given');
  72. }
  73. $this->transliterator = $callable;
  74. }
  75. /**
  76. * Get currently used transliterator callable
  77. *
  78. * @return callable
  79. */
  80. public function getTransliterator()
  81. {
  82. return $this->transliterator;
  83. }
  84. /**
  85. * Mapps additional metadata
  86. *
  87. * @param EventArgs $eventArgs
  88. * @return void
  89. */
  90. public function loadClassMetadata(EventArgs $eventArgs)
  91. {
  92. $ea = $this->getEventAdapter($eventArgs);
  93. $this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata());
  94. }
  95. /**
  96. * Generate slug on objects being updated during flush
  97. * if they require changing
  98. *
  99. * @param EventArgs $args
  100. * @return void
  101. */
  102. public function onFlush(EventArgs $args)
  103. {
  104. $ea = $this->getEventAdapter($args);
  105. $om = $ea->getObjectManager();
  106. $uow = $om->getUnitOfWork();
  107. // process all objects being inserted, using scheduled insertions instead
  108. // of prePersist in case if record will be changed before flushing this will
  109. // ensure correct result. No additional overhead is encoutered
  110. foreach ($ea->getScheduledObjectInsertions($uow) as $object) {
  111. $meta = $om->getClassMetadata(get_class($object));
  112. if ($config = $this->getConfiguration($om, $meta->name)) {
  113. // generate first to exclude this object from similar persisted slugs result
  114. $this->generateSlug($ea, $object);
  115. foreach ($config['fields'] as $slugField => $fieldsForSlugField) {
  116. $slug = $meta->getReflectionProperty($slugField)->getValue($object);
  117. $this->persistedSlugs[$config['useObjectClass']][$slugField][] = $slug;
  118. }
  119. }
  120. }
  121. // we use onFlush and not preUpdate event to let other
  122. // event listeners be nested together
  123. foreach ($ea->getScheduledObjectUpdates($uow) as $object) {
  124. $meta = $om->getClassMetadata(get_class($object));
  125. if ($config = $this->getConfiguration($om, $meta->name)) {
  126. foreach ($config['slugFields'] as $slugField) {
  127. if ($slugField['updatable']) {
  128. $this->generateSlug($ea, $object);
  129. }
  130. }
  131. }
  132. }
  133. }
  134. /**
  135. * {@inheritDoc}
  136. */
  137. protected function getNamespace()
  138. {
  139. return __NAMESPACE__;
  140. }
  141. /**
  142. * Get the slug handler instance by $class name
  143. *
  144. * @param string $class
  145. * @return Gedmo\Sluggable\Handler\SlugHandlerInterface
  146. */
  147. private function getHandler($class)
  148. {
  149. if (!isset($this->handlers[$class])) {
  150. $this->handlers[$class] = new $class($this);
  151. }
  152. return $this->handlers[$class];
  153. }
  154. /**
  155. * Creates the slug for object being flushed
  156. *
  157. * @param SluggableAdapter $ea
  158. * @param object $object
  159. * @throws UnexpectedValueException - if parameters are missing
  160. * or invalid
  161. * @return void
  162. */
  163. private function generateSlug(SluggableAdapter $ea, $object)
  164. {
  165. $om = $ea->getObjectManager();
  166. $meta = $om->getClassMetadata(get_class($object));
  167. $uow = $om->getUnitOfWork();
  168. $changeSet = $ea->getObjectChangeSet($uow, $object);
  169. $config = $this->getConfiguration($om, $meta->name);
  170. foreach ($config['fields'] as $slugField => $fieldsForSlugField) {
  171. // sort sluggable fields by position
  172. $fields = $fieldsForSlugField;
  173. usort($fields, function($a, $b) {
  174. if ($a['position'] == $b['position']) {
  175. return 1;
  176. }
  177. return ($a['position'] < $b['position']) ? -1 : 1;
  178. });
  179. $slugFieldConfig = $config['slugFields'][$slugField];
  180. // collect the slug from fields
  181. $slug = '';
  182. $needToChangeSlug = false;
  183. foreach ($fields as $sluggableField) {
  184. if (isset($changeSet[$sluggableField['field']])) {
  185. $needToChangeSlug = true;
  186. }
  187. $slug .= $meta->getReflectionProperty($sluggableField['field'])->getValue($object) . ' ';
  188. }
  189. // notify slug handlers --> onChangeDecision
  190. if (isset($config['handlers'])) {
  191. foreach ($config['handlers'] as $class => $options) {
  192. $this
  193. ->getHandler($class)
  194. ->onChangeDecision($ea, $slugFieldConfig, $object, $slug, $needToChangeSlug)
  195. ;
  196. }
  197. }
  198. // if slug is changed, do further processing
  199. if ($needToChangeSlug) {
  200. $mapping = $meta->getFieldMapping($slugFieldConfig['slug']);
  201. if (!strlen(trim($slug)) && (!isset($mapping['nullable']) || !$mapping['nullable'])) {
  202. throw new \Gedmo\Exception\UnexpectedValueException("Unable to find any non empty sluggable fields for slug [{$slugField}] , make sure they have something at least.");
  203. }
  204. // notify slug handlers --> postSlugBuild
  205. if (isset($config['handlers'])) {
  206. foreach ($config['handlers'] as $class => $options) {
  207. $this
  208. ->getHandler($class)
  209. ->postSlugBuild($ea, $slugFieldConfig, $object, $slug)
  210. ;
  211. }
  212. }
  213. // build the slug
  214. $slug = call_user_func_array(
  215. $this->transliterator,
  216. array($slug, $slugFieldConfig['separator'], $object)
  217. );
  218. // stylize the slug
  219. switch ($slugFieldConfig['style']) {
  220. case 'camel':
  221. $slug = preg_replace_callback(
  222. '@^[a-z]|' . $slugFieldConfig['separator'] . '[a-z]@smi',
  223. create_function('$m', 'return strtoupper($m[0]);'),
  224. $slug
  225. );
  226. break;
  227. default:
  228. // leave it as is
  229. break;
  230. }
  231. // cut slug if exceeded in length
  232. if (isset($mapping['length']) && strlen($slug) > $mapping['length']) {
  233. $slug = substr($slug, 0, $mapping['length']);
  234. }
  235. if (isset($mapping['nullable']) && $mapping['nullable'] && !$slug) {
  236. $slug = null;
  237. }
  238. // make unique slug if requested
  239. if ($slugFieldConfig['unique'] && !is_null($slug)) {
  240. $this->exponent = 0;
  241. $arrayConfig = $slugFieldConfig;
  242. $arrayConfig['useObjectClass'] = $config['useObjectClass'];
  243. $slug = $this->makeUniqueSlug($ea, $object, $slug, false, $arrayConfig);
  244. }
  245. // notify slug handlers --> onSlugCompletion
  246. if (isset($config['handlers'])) {
  247. foreach ($config['handlers'] as $class => $options) {
  248. $this
  249. ->getHandler($class)
  250. ->onSlugCompletion($ea, $slugFieldConfig, $object, $slug)
  251. ;
  252. }
  253. }
  254. // set the final slug
  255. $meta->getReflectionProperty($slugFieldConfig['slug'])->setValue($object, $slug);
  256. // recompute changeset
  257. $ea->recomputeSingleObjectChangeSet($uow, $meta, $object);
  258. }
  259. }
  260. }
  261. /**
  262. * Generates the unique slug
  263. *
  264. * @param SluggableAdapter $ea
  265. * @param object $object
  266. * @param string $preferedSlug
  267. * @param array $config[$slugField]
  268. * @return string - unique slug
  269. */
  270. private function makeUniqueSlug(SluggableAdapter $ea, $object, $preferedSlug, $recursing = false, $config = array())
  271. {
  272. $om = $ea->getObjectManager();
  273. $meta = $om->getClassMetadata(get_class($object));
  274. if (count ($config) == 0)
  275. {
  276. $config = $this->getConfiguration($om, $meta->name);
  277. }
  278. // search for similar slug
  279. $result = $ea->getSimilarSlugs($object, $meta, $config, $preferedSlug);
  280. // add similar persisted slugs into account
  281. $result += $this->getSimilarPersistedSlugs($config['useObjectClass'], $preferedSlug, $config['slug']);
  282. // leave only right slugs
  283. if (!$recursing) {
  284. $this->filterSimilarSlugs($result, $config, $preferedSlug);
  285. }
  286. if ($result) {
  287. $generatedSlug = $preferedSlug;
  288. $sameSlugs = array();
  289. foreach ((array)$result as $list) {
  290. $sameSlugs[] = $list[$config['slug']];
  291. }
  292. $i = pow(10, $this->exponent);
  293. do {
  294. $generatedSlug = $preferedSlug . $config['separator'] . $i++;
  295. } while (in_array($generatedSlug, $sameSlugs));
  296. $mapping = $meta->getFieldMapping($config['slug']);
  297. if (isset($mapping['length']) && strlen($generatedSlug) > $mapping['length']) {
  298. $generatedSlug = substr(
  299. $generatedSlug,
  300. 0,
  301. $mapping['length'] - (strlen($i) + strlen($config['separator']))
  302. );
  303. $this->exponent = strlen($i) - 1;
  304. $generatedSlug = $this->makeUniqueSlug($ea, $object, $generatedSlug, true, $config);
  305. }
  306. $preferedSlug = $generatedSlug;
  307. }
  308. return $preferedSlug;
  309. }
  310. /**
  311. * In case if any number of records are persisted instantly
  312. * and they contain same slugs. This method will filter those
  313. * identical slugs specialy for persisted objects. Returns
  314. * array of similar slugs found
  315. *
  316. * @param string $class
  317. * @param string $preferedSlug
  318. * @param string $slugField
  319. * @return array
  320. */
  321. private function getSimilarPersistedSlugs($class, $preferedSlug, $slugField)
  322. {
  323. $result = array();
  324. if (isset($this->persistedSlugs[$class][$slugField])) {
  325. array_walk($this->persistedSlugs[$class][$slugField], function($val) use ($preferedSlug, &$result, $slugField) {
  326. if (preg_match("@^{$preferedSlug}.*@smi", $val)) {
  327. $result[] = array($slugField => $val);
  328. }
  329. });
  330. }
  331. return $result;
  332. }
  333. /**
  334. * Filters $slugs which are matched as prefix but are
  335. * simply shorter slugs
  336. *
  337. * @param array $slugs
  338. * @param array $config
  339. * @param string $prefered
  340. */
  341. private function filterSimilarSlugs(array &$slugs, array &$config, $prefered)
  342. {
  343. foreach ($slugs as $key => $similar) {
  344. if (!preg_match("@{$prefered}($|{$config['separator']}[\d]+$)@smi", $similar[$config['slug']])) {
  345. unset($slugs[$key]);
  346. }
  347. }
  348. }
  349. }