From 63650be93d3c82f166c5e2e10ebcb56435308afa Mon Sep 17 00:00:00 2001 From: "Alex \"Pierstoval\" Ancelet" Date: Mon, 2 Mar 2015 14:46:10 +0100 Subject: [PATCH] Major update for pages. Better deletion management and cascading. Added a PageTranslation class to manage (future) translations easily. Added a "getTree()" method in the Page entity, to get the page parents' tree with a string separator (used to generate urls) --- Controller/FrontController.php | 3 +- Entity/Page.php | 109 +++++++++++++++++++++++++++------ Entity/PageTranslation.php | 49 +++++++++++++++ README.md | 38 +++++++++++- 4 files changed, 177 insertions(+), 22 deletions(-) create mode 100644 Entity/PageTranslation.php diff --git a/Controller/FrontController.php b/Controller/FrontController.php index ae3ed4d..8cee7e5 100644 --- a/Controller/FrontController.php +++ b/Controller/FrontController.php @@ -10,13 +10,11 @@ namespace Pierstoval\Bundle\CmsBundle\Controller; -use Doctrine\ORM\NonUniqueResultException; use Pierstoval\Bundle\CmsBundle\Entity\Page; use Pierstoval\Bundle\CmsBundle\Repository\PageRepository; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class FrontController extends Controller { @@ -63,6 +61,7 @@ protected function getHomepage($host = null) } throw new \Exception('No homepage has been configured. Please check your existing pages or create a homepage in your backoffice.'); } + /** * @param array $slugs * @param Page[] $pages diff --git a/Entity/Page.php b/Entity/Page.php index 26c5dc4..49c83e7 100644 --- a/Entity/Page.php +++ b/Entity/Page.php @@ -10,7 +10,9 @@ namespace Pierstoval\Bundle\CmsBundle\Entity; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; +use Doctrine\Common\Persistence\Event\LifecycleEventArgs; use Gedmo\Blameable\Traits\BlameableEntity; use Gedmo\IpTraceable\Traits\IpTraceableEntity; use Gedmo\Mapping\Annotation as Gedmo; @@ -23,7 +25,9 @@ * @ORM\Entity(repositoryClass="Pierstoval\Bundle\CmsBundle\Repository\PageRepository") * @ORM\Table(name="pierstoval_cms_pages") * @Gedmo\SoftDeleteable(fieldName="deletedAt", timeAware=false) + * @Gedmo\TranslationEntity(class="Pierstoval\Bundle\CmsBundle\Entity\PageTranslation") * @UniqueEntity("slug") + * @ORM\HasLifecycleCallbacks() */ class Page { @@ -52,7 +56,6 @@ class Page /** * @var string - * @Gedmo\Translatable * @Gedmo\Slug(fields={"title"}) * @ORM\Column(name="slug", type="string", length=255, unique=true) * @Assert\Length(max=255) @@ -125,8 +128,8 @@ class Page /** * @var Page - * @ORM\ManyToOne(targetEntity="Pierstoval\Bundle\CmsBundle\Entity\Page", inversedBy="children") - * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="CASCADE") + * @ORM\ManyToOne(targetEntity="Pierstoval\Bundle\CmsBundle\Entity\Page", inversedBy="children", fetch="EAGER") + * @ORM\JoinColumn(name="parent_id", referencedColumnName="id", onDelete="cascade") */ protected $parent; @@ -136,13 +139,24 @@ class Page */ protected $children; + /** + * @var PageTranslation[]|ArrayCollection + * @ORM\OneToMany(targetEntity="PageTranslation", mappedBy="object", cascade={"persist", "remove"}) + */ + private $translations; + public function __toString() { return $this->title; } + public function __construct() + { + $this->translations = new ArrayCollection(); + } + /** - * @return mixed + * @return string */ public function getTitle() { @@ -150,7 +164,7 @@ public function getTitle() } /** - * @param mixed $title + * @param string $title * * @return Page */ @@ -161,7 +175,7 @@ public function setTitle($title) } /** - * @return mixed + * @return string */ public function getSlug() { @@ -169,7 +183,7 @@ public function getSlug() } /** - * @param mixed $slug + * @param string $slug * * @return Page */ @@ -237,7 +251,7 @@ public function setMetaTitle($metaTitle) } /** - * @return mixed + * @return string */ public function getMetaKeywords() { @@ -245,7 +259,7 @@ public function getMetaKeywords() } /** - * @param mixed $metaKeywords + * @param string $metaKeywords * * @return Page */ @@ -275,7 +289,7 @@ public function setCategory($category) } /** - * @return mixed + * @return string */ public function getCss() { @@ -283,7 +297,7 @@ public function getCss() } /** - * @param mixed $css + * @param string $css * * @return Page */ @@ -294,7 +308,7 @@ public function setCss($css) } /** - * @return mixed + * @return string */ public function getJs() { @@ -302,7 +316,7 @@ public function getJs() } /** - * @param mixed $js + * @param string $js * * @return Page */ @@ -332,7 +346,7 @@ public function setEnabled($enabled) } /** - * @return mixed + * @return Page */ public function getParent() { @@ -340,13 +354,15 @@ public function getParent() } /** - * @param mixed $parent + * @param Page $parent * * @return Page */ - public function setParent(Page $parent) + public function setParent(Page $parent = null) { - if ($parent->getId() == $this->id) { + if ($parent && $parent->getId() == $this->id) { + // Refuse the page to have itself as parent + $this->parent = null; return $this; } $this->parent = $parent; @@ -362,7 +378,7 @@ public function getId() } /** - * @return mixed + * @return Page[] */ public function getChildren() { @@ -418,4 +434,61 @@ public function setHost($host) return $this; } + /** + * @return PageTranslation[] + */ + public function getTranslations() + { + return $this->translations; + } + + /** + * @param PageTranslation $t + * @return Page + */ + public function addTranslation(PageTranslation $t) + { + if (!$this->translations->contains($t)) { + $this->translations[] = $t; + $t->setObject($this); + } + return $this; + } + + public function getTree($separator = '/') + { + $tree = ''; + + $current = $this; + do { + $tree = $current->getSlug().$separator.$tree; + $current = $current->getParent(); + } while ($current); + + return trim($tree, $separator); + } + + /** + * @ORM\PreRemove() + * @param LifecycleEventArgs $event + */ + public function onRemove(LifecycleEventArgs $event) + { + $om = $event->getObjectManager(); + foreach ($this->translations as $translation) { + $om->remove($translation); + } + $om->flush(); + foreach ($this->children as $child) { + $child->setParent(null); + $om->persist($child); + } + $this->enabled = false; + $this->parent = null; + $this->title .= '-'.$this->id.'-deleted'; + $this->slug .= '-'.$this->id.'-deleted'; + $om->persist($this); + $om->flush(); + } + } diff --git a/Entity/PageTranslation.php b/Entity/PageTranslation.php new file mode 100644 index 0000000..53d13b4 --- /dev/null +++ b/Entity/PageTranslation.php @@ -0,0 +1,49 @@ + +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +namespace Pierstoval\Bundle\CmsBundle\Entity; + +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation; + +/** + * @ORM\Entity + * @ORM\Table(name="pierstoval_cms_pages_translations", + * uniqueConstraints={ + * @ORM\UniqueConstraint(name="lookup_unique_idx", columns={ + * "locale", "object_id", "field" + * }) + * } + * ) + */ +class PageTranslation extends AbstractPersonalTranslation +{ + + /** + * Convenient constructor + * + * @param string $locale + * @param string $field + * @param string $value + */ + public function __construct($locale, $field, $value) + { + $this->setLocale($locale); + $this->setField($field); + $this->setContent($value); + } + + /** + * @ORM\ManyToOne(targetEntity="Page", inversedBy="translations") + * @ORM\JoinColumn(name="object_id", referencedColumnName="id", onDelete="CASCADE") + */ + protected $object; + +} diff --git a/README.md b/README.md index ed00e72..2339367 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ services: ## Usage -Simply go to your backoffice in `http://127.0.0.1/admin`, and login if you are using the `Security` component. +Simply go to your backoffice in `/admin`, and login if you are using the `Security` component. You can manage `Cms Pages` and `Cms Categories` as you wish, like in an usual backoffice. @@ -153,7 +153,7 @@ The `FrontController` handles some methods to view pages with a single `indexAct The URI for a classic page is simply `/{slug}` where `slug` is the... page slug (wow, thanks captain hindsight!). -If your page has one `parent`, then the URI is the following: `/{parentSlug}/{slug}`. As the slugs are verbose nough, +If your page has one `parent`, then the URI is the following: `/{parentSlug}/{slug}`. As the slugs are verbose enough, you can notice that we respect the pages hierarchy in the generated url. You can navigate through a complex list of pages, as long as they're related as `parent` and `child`. This allows you to have such urls like this one : @@ -162,6 +162,40 @@ a parent, that has a parent, and so on, until you reach the "root" parent. ** Note: this behavior is the precise reason why you have to use a specific prefix for your `FrontController` routing import, unless you may have many "404" errors.** +### Generate a route based on a single page + +If you have a `Page` object in a view or in a controller, you can get the whole arborescence by using the `getTree()` +method, which will navigate through all parents and return a string based on a separator argument (default `/`, for urls). + +Let's get an example with this kind of tree: + +``` +/ - Home (root url) +├─ /welcome - Welcome page (set as "homepage", so "Home" will be the same) +│ ├─ /welcome/our-company - Our company +│ ├─ /welcome/our-company/financial - Financial +│ └─ /welcome/our-company/team - Team +└─ Contact +``` + +Imagine we want to generate the url for the "Team" page. You have this `Page` object in your view/controller. + +```twig + {# Page : "Team" #} + {{ path('cms_home', {"slugs": page.tree}) }} + {# Will show : /welcome/our-company/team #} +``` + +Or in a controller: + +```php + // Page : "Team" + $url = $this->generateUrl('cms_home', array('slugs' => $page->getTree())); + // $url === /welcome/our-company/team +``` + +With this, you have a functional tree system for your CMS! + ## Using FOSUserBundle to have a secured backoffice If want to use `FOSUserBundle`, you have to setup the bundle by reading [FOSUserBundle's documentation](https://github.com/FriendsOfSymfony/FOSUserBundle#documentation).