Skip to content

Instantly share code, notes, and snippets.

@duskohu
Created January 10, 2015 08:20
Show Gist options
  • Save duskohu/34aaf4c7ba690f65d4b8 to your computer and use it in GitHub Desktop.
Save duskohu/34aaf4c7ba690f65d4b8 to your computer and use it in GitHub Desktop.
TreePathListener
services:
-
class: Nas\PagesModule\Model\Listeners\TreePathListener
tags: [kdyby.subscriber]
<?php
/**
* This file is part of the Nas of Nette Framework
*
* Copyright (c) 2013 Dusan Hudak (http://dusan-hudak.com)
*
* For the full copyright and license information, please view
* the file license.txt that was distributed with this source code.
*/
namespace Nas\PagesModule\Model;
use Gedmo\Mapping\Annotation as Gedmo;
use Doctrine\ORM\Mapping as ORM;
use Kdyby\Doctrine\Entities\Attributes\Identifier;
use Nette\InvalidArgumentException;
use Nas\PagesModule\Model\Annotations\TreePath;
/**
* @Gedmo\Tree(type="nested")
* @ORM\Table(name="pages")
* @ORM\Entity(repositoryClass="Gedmo\Tree\Entity\Repository\NestedTreeRepository")
*/
class Page
{
use Identifier;
const PUBLICITY_PUBLIC = TRUE;
const PUBLICITY_NO_PUBLIC = FALSE;
/**
* @ORM\Column(name="title", type="string", length=255)
* @var string
*/
private $title;
/**
* @Gedmo\TreeLeft
* @ORM\Column(name="lft", type="integer")
* @var int
*/
private $lft;
/**
* @Gedmo\TreeLevel
* @ORM\Column(name="lvl", type="integer")
* @var int
*/
private $lvl;
/**
* @Gedmo\TreeRight
* @ORM\Column(name="rgt", type="integer")
* @var int
*/
private $rgt;
/**
* @Gedmo\TreeRoot
* @ORM\Column(name="root", type="integer", nullable=true)
* @var int
*/
private $root;
/**
* @Gedmo\TreeParent
* @ORM\ManyToOne(targetEntity="Page", inversedBy="children")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE")
* @var int
*/
private $parent;
/**
* @ORM\OneToMany(targetEntity="Page", mappedBy="parent")
* @ORM\OrderBy({"lft" = "ASC"})
* @var int
*/
private $children;
/**
* @ORM\Column(name="publicity", type="boolean")
* @var bool
*/
private $publicity;
/**
* @ORM\Column(name="keywords", type="text", nullable=true)
* @var string
*/
private $keywords;
/**
* @ORM\Column(name="context", type="text", nullable=true)
* @var string
*/
private $context;
/**
* @ORM\Column(name="description", type="text", nullable=true)
* @var string
*/
private $description;
/**
* @ORM\Column(name="slug", type="text", nullable=true)
* @var string
*/
private $slug;
/**
* @Nas\PagesModule\Model\Annotations\TreePath(slugField="slug", separator="/", parentRelationField="parent")
* @ORM\Column(name="path", type="string", length=255, unique=true))
*/
private $path;
/**
* @param string $title
*/
public function setTitle($title)
{
$this->title = $title;
}
/**
* @return int string
*/
public function getTitle()
{
return $this->title;
}
/**
* @param Page $parent
*/
public function setParent(Page $parent = NULL)
{
$this->parent = $parent;
}
/**
* @return Page|NULL
*/
public function getParent()
{
return $this->parent;
}
/**
* @return \Doctrine\ORM\PersistentCollection
*/
public function getChildren()
{
return $this->children;
}
/**
* @return int
*/
public function getRoot()
{
return $this->root;
}
/**
* @return int
*/
public function getLvl()
{
return $this->lvl;
}
/**
* @return int
*/
public function getRgt()
{
return $this->rgt;
}
/**
* @return int
*/
public function getLft()
{
return $this->lft;
}
/**
* @param bool $publicity
* @throws \Nette\InvalidArgumentException
*/
public function setPublicity($publicity)
{
if ($publicity !== self::PUBLICITY_PUBLIC && $publicity !== self::PUBLICITY_NO_PUBLIC) {
throw new InvalidArgumentException('Parameter $publicity can be only "' . __CLASS__ . '::PUBLICITY_PUBLIC" or "' . __CLASS__ . '::PUBLICITY_NO_PUBLIC"');
}
$this->publicity = $publicity;
}
/**
* @return bool
*/
public function getPublicity()
{
return $this->publicity;
}
/**
* @return string
*/
public function getKeywords()
{
return $this->keywords;
}
/**
* @param string $keywords
*/
public function setKeywords($keywords)
{
$this->keywords = $keywords;
}
/**
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* @param string $description
*/
public function setDescription($description)
{
$this->description = $description;
}
/**
* @return string
*/
public function getContext()
{
return $this->context;
}
/**
* @param string $context
*/
public function setContext($context)
{
$this->context = $context;
}
/**
* @return string
*/
public function getSlug()
{
return $this->slug;
}
/**
* @param string $slug
*/
public function setSlug($slug)
{
$this->slug = $slug;
}
/**
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* @return string
*/
public static function getClassName()
{
return __CLASS__;
}
}
<?php
/**
* This file is part of the Nas of Nette Framework
*
* Copyright (c) 2013 Dusan Hudak (http://dusan-hudak.com)
*
* For the full copyright and license information, please view
* the file license.txt that was distributed with this source code.
*/
namespace Nas\PagesModule\Model\Annotations;
use Doctrine\Common\Annotations\Annotation;
/**
* TreePath annotation for TreePathListener
*
* @Annotation
* @Target("PROPERTY")
*
* @author Dusan Hudak <[email protected]>
*/
final class TreePath extends Annotation
{
/** @var string @required */
public $slugField;
/** @var string @required */
public $separator;
/** @var string @required */
public $parentRelationField;
}
<?php
/**
* This file is part of the Nas of Nette Framework
*
* Copyright (c) 2013 Dusan Hudak (http://dusan-hudak.com)
*
* For the full copyright and license information, please view
* the file license.txt that was distributed with this source code.
*/
namespace Nas\PagesModule\Model\Listeners;
use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\Query;
use Kdyby\Events\Subscriber;
/**
* @author Dusan Hudak <[email protected]>
*/
class TreePathListener implements Subscriber
{
const SLUG_SEPARATOR = '-';
/**
* Specifies the list of events to listen
*
* @return array
*/
public function getSubscribedEvents()
{
return array(
Events::onFlush,
);
}
/**
* @param $entity
* @param EntityManager $em
* @return array
*/
private function getPathColumns($entity, EntityManager $em)
{
$meta = $em->getClassMetadata(get_class($entity));
$reader = new AnnotationReader();
$reader->getClassAnnotations($meta->getReflectionClass());
$pathColumns = array();
foreach ($meta->getReflectionProperties() as $name => $property) {
$annotation = $reader->getPropertyAnnotation($property, 'Nas\PagesModule\Model\Annotations\TreePath');
if ($annotation !== NULL) {
$pathColumns[$name] = $annotation;
}
}
return $pathColumns;
}
/**
* @param EntityManager $em
* @param object $object
* @param string $path
* @param string $columnName
* @return array
*/
private function getSimilarPaths($em, $path, $columnName, $object)
{
$meta = $em->getClassMetadata(get_class($object));
$reflectionClass = $meta->getReflectionClass();
$uow = $em->getUnitOfWork();
$qb = $em->createQueryBuilder();
$qb->select('rec.' . $columnName)
->from($reflectionClass->name, 'rec')
->where($qb->expr()->like(
'rec.' . $columnName,
$qb->expr()->literal($path . '%'))
);
// Find also path without increment
$pos = strrpos($path, self::SLUG_SEPARATOR);
if ($pos !== FALSE) {
$number = substr($path, $pos + 1);
if (is_numeric($number)) {
$pos = strrpos($path, self::SLUG_SEPARATOR);
if ($pos !== FALSE) {
$qb->orWhere($qb->expr()->like(
'rec.' . $columnName,
$qb->expr()->literal(substr($path, 0, $pos) . '%'))
);
}
}
}
// Without actual object
$identifierValue = $uow->getSingleIdentifierValue($object);
if ($identifierValue) {
$qb->andWhere('rec.' . $meta->identifier[0] . ' != :identifier');
$qb->setParameter('identifier', $uow->getSingleIdentifierValue($object));
}
$q = $qb->getQuery();
$q->setHydrationMode(Query::HYDRATE_ARRAY);
return $q->execute();
}
/**
* @param EntityManager $em
* @param string $path
* @param string $columnName
* @param object $object
* @return string
*/
private function regeneratePath($em, $path, $columnName, $object)
{
$similarPath = $this->getSimilarPaths($em, $path, $columnName, $object);
$generatedPath = $path;
if (!empty($similarPath)) {
$samePath = array();
foreach ($similarPath as $similar) {
$samePath[] = $similar[$columnName];
}
$i = 1;
// If path have of the end number remove number from path
$pos = strrpos($path, self::SLUG_SEPARATOR);
if ($pos !== FALSE) {
$number = substr($path, $pos + 1);
if (is_numeric($number)) {
$i = $number;
$pos = strrpos($path, self::SLUG_SEPARATOR);
if ($pos !== FALSE) {
$path = substr($path, 0, $pos);
}
}
}
if (in_array($generatedPath, $samePath)) {
do {
$generatedPath = $path . self::SLUG_SEPARATOR . $i++;
} while (in_array($generatedPath, $samePath));
}
}
return $generatedPath;
}
/**
* @param EntityManager $em
* @param string $oldPath
* @param string $newPath
* @param string $columnName
* @param object $object
* @return mixed
*/
public function replaceRelative($em, $oldPath, $newPath, $columnName, $object)
{
$meta = $em->getClassMetadata(get_class($object));
$reflectionClass = $meta->getReflectionClass();
$qb = $em->createQueryBuilder();
$qb->update($reflectionClass->name, 'rec')
->set('rec.' . $columnName, $qb->expr()->concat(
$qb->expr()->literal($newPath),
$qb->expr()->substring('rec.' . $columnName, strlen($oldPath) + 1)
))
->where($qb->expr()->like(
'rec.' . $columnName,
$qb->expr()->literal($oldPath . '%'))
);
$q = $qb->getQuery();
return $q->execute();
}
/**
* Generate slug on objects being updated during flush
* if they require changing
*
* @param OnFlushEventArgs $args
* @return void
*/
public function onFlush(OnFlushEventArgs $args)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
// Process Insertions
foreach ($uow->getScheduledEntityInsertions() as $key => $object) {
$pathColumns = $this->getPathColumns($object, $em);
if (!empty($pathColumns)) {
$meta = $em->getClassMetadata(get_class($object));
foreach ($pathColumns as $name => $property) {
$slug = $meta->getReflectionProperty($property->slugField)->getValue($object);
$separator = $property->separator;
$parentRelationField = $meta->getReflectionProperty($property->parentRelationField)->getValue($object);
$parentPath = '';
if ($parentRelationField) {
$parentPath = $meta->getReflectionProperty($name)->getValue($parentRelationField);
}
$path = ($parentPath !== $separator) ? $parentPath . $separator : $parentPath;
$path .= $slug;
// Regenerate Path
$newPath = $this->regeneratePath($em, $path, $name, $object);
// Change slug if change path
if ($newPath !== $path) {
$pos = strrpos($newPath, $separator);
if ($pos !== FALSE) {
$uow->propertyChanged($object, $property->slugField, $slug, substr($newPath, $pos + 1));
}
}
$oldPath = $meta->getReflectionProperty($name)->getValue($object);
$uow->propertyChanged($object, $name, $oldPath, $newPath);
}
}
}
// Process Updates
foreach ($uow->getScheduledEntityUpdates() as $key => $object) {
$pathColumns = $this->getPathColumns($object, $em);
if (!empty($pathColumns)) {
$meta = $em->getClassMetadata(get_class($object));
foreach ($pathColumns as $name => $property) {
$slug = $meta->getReflectionProperty($property->slugField)->getValue($object);
$separator = $property->separator;
$parentRelationField = $meta->getReflectionProperty($property->parentRelationField)->getValue($object);
$parentPath = '';
if ($parentRelationField) {
$parentPath = $meta->getReflectionProperty($name)->getValue($parentRelationField);
}
$path = ($parentPath !== $separator) ? $parentPath . $separator : $parentPath;
$path .= $slug;
// Regenerate Path
$newPath = $this->regeneratePath($em, $path, $name, $object);
// Change slug if change path
if ($newPath !== $path) {
$pos = strrpos($newPath, $separator);
if ($pos !== FALSE) {
$uow->propertyChanged($object, $property->slugField, $slug, substr($newPath, $pos + 1));
}
}
$oldPath = $meta->getReflectionProperty($name)->getValue($object);
$uow->propertyChanged($object, $name, $oldPath, $newPath);
//Change path of child items
$this->replaceRelative($em, $oldPath, $newPath, $name, $object);
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment