Symfony2: Where place slug and timestamp methods? - php

I am coding a service which will handle articles (CRUD).
The persistence layer is handles by an ArticleManager >which does Repository and CRUD actions.
Now I want to implement two attributes: createdAt and >updatedAt
My question is now where I should place them:
In the entity, in the ArticleManager, somewhere else?
Best Regards,
Bodo
Ah,
I see, the FOSUserBundle handles this task with an EventListener:
https://github.com/FriendsOfSymfony/FOSUserBundle/blob/master/Entity/UserListener.php
But thank you for youre help :)
<?php
namespace LOC\ArticleBundle\Entity;
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Events;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreUpdateEventArgs;
use LOC\ArticleBundle\Model\ArticleInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
class ArticleListener implements EventSubscriber
{
private $articleManager;
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getSubscribedEvents()
{
return array(
Events::prePersist,
Events::preUpdate,
);
}
public function prePersist(LifecycleEventArgs $args)
{
$article = $args->getEntity();
$article->setCreatedAt(new \DateTime());
$this->articleManager->updateArticle($article);
}
public function preUpdate(PreUpdateEventArgs $args)
{
$article = $args->getEntity();
$article->setUpdatedAt(new \DateTime());
$this->articleManager->updateArticle($article);
}
}

Well, there is a bundle for such stuff, the DoctrineExtensionsBundle. It got Timestampable and slugable.
If you want to do it on your own, the place is definitly in the Entity itself, as you don't want to mess around in your controller. Here is how I do the Timestampable as I don't use the DoctrineExtensionsBundle:
/**
* #ORM\Entity
* #ORM\Table(name="entity")
* #ORM\HasLifecycleCallbacks
*/
class Entity {
// ...
/**
* #ORM\Column(name="created_at", type="datetime", nullable=false)
*/
protected $createdAt;
/**
* #ORM\Column(name="updated_at", type="datetime", nullable=false)
*/
protected $updatedAt;
/**
* #ORM\prePersist
*/
public function prePersist() {
$this->createdAt = new \DateTime();
$this->updatedAt = new \DateTime();
}
/**
* #ORM\preUpdate
*/
public function preUpdate() {
$this->updatedAt = new \DateTime();
}
// ...
}
As for my decision not to use the Bundle: When symfony2 was released as stable, this bundle didn't exist (or it wasn't stable, I don't remember) so I started doing it on my own like shown below. As it is little overhead, I kept doing it like this and never felt the need to change it. If you need Slugable or want to keep it simply, try the bundle!

In the entity, since that's where they belong logically.

Related

Doctrine lifecycle callbacks not working with EasyAdminBundle

I am using Symfony 4 with EasyAdminBundle to create a simple administrative interface. I'm having a few problems trying to automatically set the value for createdAt and updatedAt columns. When creating/updating an entity within EasyAdmin, the configured Doctrine lifecycle callbacks are not called. For example, here's a simple entity that is managed with EasyAdmin, note the lifecycle callback hooks:
<?php
namespace App\Entity;
/**
* #ORM\Entity(repositoryClass="App\Repository\ProductRepository")
* #ORM\HasLifecycleCallbacks
*/
class Product
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
// Additional column configuration removed for brevity
/**
* #ORM\Column(type="datetime")
*/
private $createdAt;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $updatedAt;
// Additional getters/setters removed for brevity
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(?\DateTimeInterface $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updatedAt;
}
public function setUpdatedAt(?\DateTimeInterface $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* #ORM\PrePersist
*/
public function onPrePersist(): void
{
$this->createdAt = new \DateTime();
}
/**
* #ORM\PreUpdate
*/
public function onPreUpdate(): void
{
$this->updatedAt = new \DateTime();
}
}
When I create a new product within EasyAdmin, onPrePersist() is not called, and when I edit an existing product with EasyAdmin, onPreUpdate() is not called.
If I create a new product the "traditional" way, the lifecycle callbacks work exactly as expected. For example:
<?php
$product = new Product();
$product->setTitle('Test Product');
$product->setDescription('Test description');
// Doctrine lifecycle callbacks work as expected
$this->getDoctrine()->getManager()->persist($product);
Is EasyAdminBundle bypassing Doctrine lifecycle callbacks? If so, why? How do I utilize Doctrine lifecycle callbacks within Doctrine entities managed by EasyAdminBundle?
I understand there is documentation to do something similar with an AdminController:
https://symfony.com/doc/master/bundles/EasyAdminBundle/book/complex-dynamic-backends.html#update-some-properties-for-all-entities
But again, why do I need to do this when we already have Doctrine lifecycle callbacks. My other problem with using an AdminController and extending various methods, persistEntity() within AdminController is never called either.
What am I missing?
Any help would be greatly appreciated! Cheers!

Logging from inside a repository in Symfony

I'm tracing a weird error in a Symfony 2 app and I'd like to know if there's a way to print log messages from a Repository PHP file. For example:
class OrderEntityRepository extends EntityRepository
{
/**
*
* #param mixed $filter
* #return type
*/
public function findByCriteria($filter) {
[...]
/* I'D LIKE TO LOG SOME VARIABLES FROM HERE */
}
}
I've tried using error_log() but nothing happens.
Is this possible? Thanks in advance,
It's possible but it's usually not a good practice. The good thing to do is to send back the Repository result to your Controller or Service and you log from them an error or something else.
But if you still want to do it, Repository are like services (when you implements ServiceEntityRepository see this slide for more information). If you want to log something specific inside you have to inject the LoggerInterface into your Repository configuration (like you do with service).
In your service.yml (or xml) if you don't use autowire:
Your\Repository:
arguments: ['#logger']
In your repository class:
/**
* #var LoggerInterface
*/
protected $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
On symfony 3.8 I have
class BlahRepository extends ServiceEntityRepository
{
/* #var ContainerInterface $container */
private $container;
/* #var LoggerInterface $logger */
private $logger;
public function __construct(RegistryInterface $registry, ContainerInterface $container, LoggerInterface $logger)
{
parent::__construct($registry, Blah::class);
$this->container = $container;
$this->logger = $logger;
}
}
and I am able to use $this->logger->info("text")
I think the trick may be extending ServiceEntityRepository
In order to use dependency injection for Doctrine entity repositories, you can create a custom RepositoryFactory.
Tested on Symfony 3.4.
<?php
namespace AppBundle\Doctrine;
use Doctrine\Common\Persistence\ObjectRepository;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Repository\DefaultRepositoryFactory;
use Doctrine\ORM\Repository\RepositoryFactory as RepositoryFactoryInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
class RepositoryFactory implements RepositoryFactoryInterface, LoggerAwareInterface
{
/** #var DefaultRepositoryFactory */
protected $defaultRepositoryFactory;
/** #var LoggerInterface */
private $logger;
/**
* #required (for autowiring)
* #param LoggerInterface $logger (Monolog will be the default one)
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* #see Configuration::getRepositoryFactory()
*/
public function __construct()
{
$this->defaultRepositoryFactory = new DefaultRepositoryFactory();
}
/**
* Gets the repository for an entity class.
*
* #param EntityManagerInterface $entityManager
* #param string $entityName The name of the entity.
* #return ObjectRepository
*/
public function getRepository(EntityManagerInterface $entityManager, $entityName): ObjectRepository
{
$repository = $this->defaultRepositoryFactory->getRepository($entityManager, $entityName);
if ($repository instanceof LoggerAwareInterface && $this->logger !== null) {
$repository->setLogger($this->logger);
}
return $repository;
}
}
Declare it in Doctrine configuration.
# app/config.yml
doctrine:
# ...
orm:
repository_factory: AppBundle\Doctrine\RepositoryFactory
And finally, make your repository class implement LoggerAwareInterface.
class OrderEntityRepository extends EntityRepository implements LoggerAwareInterface
{
/** #var LoggerInterface */
private $logger;
/**
* #param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/**
* #param mixed $filter
* #return type
*/
public function findByCriteria($filter) {
//[...]
$this->logger->alert('message');
}
}
You can also make a LoggerAwareTrait trait to spare yourself some code duplication.

Symfony4 extends controller with route annotation

I'm building a webapp with Symfony and since now I had to repeat a specific pattern for each new controller I built.
For example I have this AdminController :
/**
* #Route("/pro/{uniqid}")
* #ParamConverter("company", options={"mapping":{"uniqid" = "uniqid"}})
* #Security("is_granted(constant('App\\Security\\Voter\\CompanyVoter::VIEW'), company)")
* #package App\Controller
*/
class AdminController extends Controller
{
/**
* #Route("/admin/users/", name="users")
* #return \Symfony\Component\HttpFoundation\Response
*/
public function users(Company $company){}
}
So, each controller must redefine #Route, #ParamConverter and #Security that is extremely redundant.
I tried to create a LoggedController that define every annotation, then make Controller extends that LoggedController, but that does not work.
Is there a solution or should I continue to copy/paste these Annotation each time I create a new Controller that needs to implement it ?
EDIT :
I add the declaration of Company entity :
/**
* #ORM\Entity(repositoryClass="App\Repository\CompanyRepository")
*/
class Company
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
Long story short, you can but it will be a lot easier to duplicate your annotations in every controller.
But if you wan't to do this anyway, here are some solutions.
Routing
This is the easy one. You can define a global prefix in the config/routes/annotations.yaml file.
If you're using the default config, you can try something like this:
# Default controllers
controllers:
resource: ../../src/Controller/
type: annotation
# Company controllers
company_controllers:
resource: ../../src/Controller/Company/
type: annotation
prefix: /pro/{uniqid}
All your routes will now start with /pro/{uniqid} and you can remove the #Route annotation from your controller.
ParamConverter
You can create your own ParamConverter. Everytime you'll use a Company type in an action method, it'll be converted to the matching entity using the uniqid attribute.
Something like this:
// src/ParamConverter/CompanyConverter.php
<?php
namespace App\ParamConverter;
use App\Entity\Company;
use Doctrine\ORM\EntityManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
use Symfony\Component\HttpFoundation\Request;
class CompanyConverter implements ParamConverterInterface
{
const CONVERTER_ATTRIBUTE = 'uniqid';
/**
* #var EntityManagerInterface
*/
private $entityManager;
/**
* CompanyConverter constructor.
*
* #param EntityManagerInterface $entityManager
*/
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* #inheritdoc
*/
public function apply(Request $request, ParamConverter $configuration)
{
$uniqid = $request->attributes->get(self::CONVERTER_ATTRIBUTE);
$company = $this->entityManager->getRepository(Company::class)->findOneBy(['uniqid' => $uniqid]);
$request->attributes->set($configuration->getName(), $company);
}
/**
* #inheritdoc
*/
function supports(ParamConverter $configuration)
{
return $configuration->getClass() === Company::class;
}
}
With this, you can remove the #ParamConverter annotation from your controller.
Security
You can't use the access_control section of the security.yaml file since custom functions are not yet supported.
Otherwise, something like this could have been nice:
security:
...
access_control:
-
path: ^/pro
allow_if: "is_granted(constant('App\\Security\\Voter\\CompanyVoter::VIEW'), company)"
(Note: It was fixed in Symfony 4.1 but i don't know yet how it will work).
Instead, you can use a subscriber listening on the kernel.request kernel event:
<?php
namespace App\Subscriber;
use App\Entity\Company;
use App\Security\CompanyVoter;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
class SecurityListener implements EventSubscriberInterface
{
/**
* #var AuthorizationCheckerInterface
*/
private $authorizationChecker;
/**
* #var EntityManagerInterface
*/
private $entityManager;
/**
* #param AuthorizationCheckerInterface $authorizationChecker
* #param EntityManagerInterface $entityManagerInterface
*/
public function __construct(AuthorizationCheckerInterface $authorizationChecker, EntityManagerInterface $entityManager)
{
$this->authorizationChecker = $authorizationChecker;
$this->entityManager = $entityManager;
}
/**
* #param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$uniqid = $request->attributes->get('uniqid')) {
return;
}
$company = $this->entityManager->getRepository(Company::class)->findOneBy(['titre' => $uniqid]);
if (!$this->authorizationChecker->isGranted(CompanyVoter::VIEW, $company)) {
throw new AccessDeniedHttpException();
}
}
/**
* #return array
*/
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => 'onKernelRequest',
);
}
}

Calling a service inside a lifecycle event

I have a lifecycle event. As soon as an order is created the prePersist lifecycle event add a few more details to the order before it is persisted to the database.
This is my prePersist event class;
<?php
namespace Qi\Bss\BaseBundle\Lib\PurchaseModule;
use Qi\Bss\BaseBundle\Entity\Business\PmodOrder;
use Doctrine\ORM\Event\LifecycleEventArgs;
/**
* Listener class
* Handles events related to list prices
*/
class OrderUserListener
{
/**
* Service container
* #var type
*/
private $serviceContainer;
/**
* Performs tasks before destruction
* #ORM\PrePersist
*/
public function prePersist(LifecycleEventArgs $args)
{
$order = $args->getEntity();
if ($order instanceof PmodOrder) {
$user = $this->serviceContainer->get('security.token_storage')->getToken()->getUser();
if ($user) {
$order->setCreatedBy($user);
$order->setCreatedAt(new \DateTime(date('Y-m-d H:i:s')));
$order->setDepartment($user->getDepartment());
$order->setStatus(PmodOrder::STATUS_AWAITING_APPROVAL);
$this->serviceContainer->get('bss.pmod.order_logger')->log($order, 'Order Created');
}
}
}
/**
* Sets the sales order exporter object
* #param type $serviceContainer
*/
public function setServiceContainer($serviceContainer)
{
$this->serviceContainer = $serviceContainer;
}
}
It works perfectly but this part $this->serviceContainer->get('bss.pmod.order_logger')->log($order, 'Order Created'); doesn't want to work. I try to call a service inside it. I know the service works perfectly inside my controllers, but here I get an error;
A new entity was found through the relationship
'Qi\Bss\BaseBundle\Entity\Business\PmodLog#order' that was not
configured to cascade persist operations for entity: Nuwe Test vir
logger. To solve this issue: Either explicitly call
EntityManager#persist() on this unknown entity or configure cascade
persist this association in the mapping for example
#ManyToOne(..,cascade={"persist"}).
This is how my OrderLogger service class looks like;
<?php
namespace Qi\Bss\BaseBundle\Lib\PurchaseModule;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Doctrine\ORM\EntityManager;
use Qi\Bss\BaseBundle\Entity\Business\PmodLog;
/**
* Class AppLogger. Purchase Module logger.
* #package FcConnectBundle\Lib
*/
class OrderLogger {
private $em;
private $tokenStorage;
/**
* Constructor.
*
* #param EntityManager $em
* #param TokenStorage $securityTokenStorage
*/
public function __construct(EntityManager $em, TokenStorage $securityTokenStorage)
{
$this->em = $em;
$this->tokenStorage = $securityTokenStorage;
}
/**
* Log an order action.
*
* #param string $text
*/
public function log($order, $action)
{
$logRecord = new PmodLog();
if (is_object($this->tokenStorage->getToken())) {
$user = $this->tokenStorage->getToken()->getUser();
if (is_object($user)) {
$logRecord->setUser($user);
}
}
$logRecord->setOrder($order);
$logRecord->setAction($action);
$logRecord->setTime(new \DateTime());
$this->em->persist($logRecord);
$this->em->flush();
}
}
I have already tried changing the persist in my log to merge, but that also doesn't work. Can somebody please help and explain what I do wrong?
This is not the best architecture, but it will work:
On prePersist add all messages to some kind of private variable (like $logMessages), and add another event
/**
* #param PostFlushEventArgs $args
*/
public function postFlush(PostFlushEventArgs $args)
{
$logMessages = $this->logMessages;
$this->logMessages = array(); //clean to avoid double logging
if (!empty($logMessages)) {
foreach ($logMessages as $message) {
$this->serviceContainer->get('bss.pmod.order_logger')->log($message);
}
}
}
I fixed the problem by adding a postPersist and call the logger in there instead of inside my prePersist;
/**
* Performs tasks before destruction
* #ORM\PostPersist
*/
public function postPersist(LifecycleEventArgs $args)
{
$order = $args->getEntity();
if ($order instanceof PmodOrder) {
$this->serviceContainer->get('bss.pmod.order_logger')->log($order, 'Order Created');
}
}
Because what I think is happening is that the logger tries to be executed but the order in the logger doesn't yet exists as it is not yet persisted. This way makes more sense to me, and I think this is the easiest fix. I could be wrong though, any comments and other opinions on my answer are welcome.

PreAuthorize annotation of Symfony2 JMSSecurityExtraBundle not working

Using the JMSSecurityExtraBundle of Symfony2 I try to create my own expression method and bind it in a controller using the PreAuthorize annotation.
I don't know why but the method is never fired, and the security bundle while trying to evaluate the PreAuthorize annotation concludes with a "Token does not have the required roles.". Seems like is trying to validate roles and not to resolve the PreAuthorize expression.
Example about what I'm trying to do:
<?php
namespace Acme\HelperBundle\Security;
use Symfony\Component\DependencyInjection\ContainerInterface;
use JMS\DiExtraBundle\Annotation as DI;
/** #DI\Service */
class RequestAccessEvaluator
{
private $container;
/**
* #DI\InjectParams({
* "container" = #DI\Inject("service_container"),
* })
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/** #DI\SecurityFunction("isAllowed") */
public function isAllowed()
{
return true;
}
}
My Controller:
/**
*
* #PreAuthorize("isAllowed()")
* #Route("/bla/{id}")
* #Method({"POST"})
* #return json
*/
public function postBlaAction(Request $request, $id)
{
I finally solved my problem... actually I missed that config.
It worked just putting that in my config.yml and setting the option "expressions" to true.

Categories