I'm trying to move my heavy treatment to Messenger.
It works but I would like to show a percentage of progress to my users.
I created a DownloadTask entity and I try to update it during the process but it doesn't work. My entity is not updated in the database.
Do you have any ideas?
I'm still learning, so I hope my code won't hurt your eyes too much.
<?php
namespace App\MessageHandler;
use App\Message\GetSellerList;
use App\Repository\DownloadTaskRepository;
use App\Service\EbayDL;
use Doctrine\DBAL\ConnectionException;
use Doctrine\ORM\EntityManagerInterface;
use DTS\eBaySDK\Trading\Types\GetSellerListResponseType;
use GuzzleHttp\Promise;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
class GetSellerListHandler implements MessageHandlerInterface
{
/**
* #var EbayDL
*/
private $ebayDL;
/**
* #var EntityManagerInterface
*/
private $entityManager;
/**
* #var LoggerInterface
*/
private $logger;
/**
* #var \App\Entity\DownloadTask
*/
private $task;
/**
* #var DownloadTaskRepository
*/
private $downloadTaskRepository;
public function __construct(EbayDL $ebayDL, EntityManagerInterface $entityManager, DownloadTaskRepository $downloadTaskRepository, LoggerInterface $logger)
{
$this->ebayDL = $ebayDL;
$this->entityManager = $entityManager;
$this->logger = $logger;
$this->downloadTaskRepository = $downloadTaskRepository;
}
/**
* #throws ConnectionException
* #throws \Exception
* #throws \Throwable
*/
public function __invoke(GetSellerList $getSellerList): void
{
$task = $this->downloadTaskRepository->find($getSellerList->getDownloadTaskId());
$this->clearDatabase();
$listingInfos = $this->ebayDL->getInformation();
$totalNumberOfPages = $listingInfos['totalNumberOfPages'];
$promises = (function () use ($totalNumberOfPages) {
for ($page = 1; $page <= $totalNumberOfPages; ++$page) {
yield $this->ebayDL->getProductsByPageAsync($page);
}
})();
$eachPromise = new Promise\EachPromise($promises, [
'concurrency' => 6,
'fulfilled' => function (GetSellerListResponseType $response): void {
$products = $this->ebayDL->parseSellerListResponse($response);
foreach ($products as $product) {
try {
$this->entityManager->persist($product);
$this->entityManager->flush();
} catch (\Exception $e) {
$this->logger->error('Failed to store a product');
$this->logger->error($e->getMessage());
if (!$this->entityManager->isOpen()) {
// https://stackoverflow.com/questions/14258591/the-entitymanager-is-closed
$this->entityManager = $this->entityManager->create(
$this->entityManager->getConnection(),
$this->entityManager->getConfiguration()
);
}
}
}
},
]);
$eachPromise->promise()->wait();
$this->task
->setProgress(100)
->setCompletedAt(new \DateTime('NOW'));
$this->entityManager->flush();
}
/**
* #throws ConnectionException
* #throws \Doctrine\DBAL\DBALException
*/
private function clearDatabase(): void
{
$connection = $this->entityManager->getConnection();
$connection->beginTransaction();
try {
$connection->query('SET FOREIGN_KEY_CHECKS=0');
$connection->query('DELETE FROM product');
$connection->query('SET FOREIGN_KEY_CHECKS=1');
$connection->commit();
} catch (\Exception $exception) {
$connection->rollback();
}
}
}
Should
$task = $this->downloadTaskRepository->find($getSellerList->getDownloadTaskId());
be
$this->task = $this->downloadTaskRepository->find($getSellerList->getDownloadTaskId());
?
Related
Unfortunately, so far I am a complete beginner in creating a module in magento.
I need to send an email after adding the item to the cart.
As I understand it, I need to use the checkout_cart_product_add_after event
I created some files, but I don't understand how to send an email after adding the item to the cart
My/Module/etc/events.xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="checkout_cart_product_add_after">
<observer name="email_after_adding_product" instance="My\Module\Observer\SendEmailForCart"/>
</event>
My/Module/Observer/SendEmailForCart.php
<?php
namespace My\Module\Observer;
use Magento\Framework\Event\ObserverInterface;
use My\Module\Helper\Email;
class SendEmailForCart implements ObserverInterface
{
private $helperEmail;
public function __construct(
Email $helperEmail
) {
$this->helperEmail = $helperEmail;
}
public function execute(\Magento\Framework\Event\Observer $observer)
{
return $this->helperEmail->sendEmail();
}
}
My/Module/Helper/Email.php
<?php
namespace My\Module\Helper;
class Email extends \Magento\Framework\App\Helper\AbstractHelper
{
public function __construct(
)
{
}
public function sendEmail()
{
try {
} catch (\Exception $e) {
}
}
}
Please tell, what code I need to write in the Email.php file?
And do I need to create any additional files or modify the ones I showed above?
/**
* Recipient email config path
*/
const XML_PATH_EMAIL_RECIPIENT = 'contact/email/recipient_email';
/**
* #var \Magento\Framework\Mail\Template\TransportBuilder
*/
protected $_transportBuilder;
/**
* #var \Magento\Framework\Translate\Inline\StateInterface
*/
protected $inlineTranslation;
/**
* #var \Magento\Framework\App\Config\ScopeConfigInterface
*/
protected $scopeConfig;
/**
* #var \Magento\Store\Model\StoreManagerInterface
*/
protected $storeManager;
/**
* #var \Magento\Framework\Escaper
*/
protected $_escaper;
/**
* #param \Magento\Framework\App\Action\Context $context
* #param \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder
* #param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation
* #param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
* #param \Magento\Store\Model\StoreManagerInterface $storeManager
*/
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Magento\Framework\Mail\Template\TransportBuilder $transportBuilder,
\Magento\Framework\Translate\Inline\StateInterface $inlineTranslation,
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
\Magento\Store\Model\StoreManagerInterface $storeManager,
\Magento\Framework\Escaper $escaper
) {
parent::__construct($context);
$this->_transportBuilder = $transportBuilder;
$this->inlineTranslation = $inlineTranslation;
$this->scopeConfig = $scopeConfig;
$this->storeManager = $storeManager;
$this->_escaper = $escaper;
}
/**
* Post user question
*
* #return void
* #throws \Exception
*/
public function execute()
{
$post = $this->getRequest()->getPostValue();
if (!$post) {
$this->_redirect('*/*/');
return;
}
$this->inlineTranslation->suspend();
try {
$postObject = new \Magento\Framework\DataObject();
$postObject->setData($post);
$error = false;
$sender = [
'name' => $this->_escaper->escapeHtml($post['name']),
'email' => $this->_escaper->escapeHtml($post['email']),
];
$storeScope = \Magento\Store\Model\ScopeInterface::SCOPE_STORE;
$transport = $this->_transportBuilder
->setTemplateIdentifier('send_email_email_template') // this code we have mentioned in the email_templates.xml
->setTemplateOptions(
[
'area' => \Magento\Framework\App\Area::AREA_FRONTEND, // this is using frontend area to get the template file
'store' => \Magento\Store\Model\Store::DEFAULT_STORE_ID,
]
)
->setTemplateVars(['data' => $postObject])
->setFrom($sender)
->addTo($this->scopeConfig->getValue(self::XML_PATH_EMAIL_RECIPIENT, $storeScope))
->getTransport();
$transport->sendMessage();
$this->inlineTranslation->resume();
$this->messageManager->addSuccess(
__('Thanks for contacting us with your comments and questions. We\'ll respond to you very soon.')
);
$this->_redirect('*/*/');
return;
} catch (\Exception $e) {
$this->inlineTranslation->resume();
$this->messageManager->addError(__('We can\'t process your request right now. Sorry, that\'s all we know.'.$e->getMessage())
);
$this->_redirect('*/*/');
return;
}
}
Make a reasearch on your own in vendor/magento. There are many places where emails are sent. Look for $transport or $transportObject variable (don't remember)
I create service for add formType then persist object and in controller I invoke data but I have error shown on below image:
in controller i extend class abstractController content getHandler and I have view newskill.html.twig
Code SkillController.php:
<?php
namespace AppBundle\Controller\Condidate;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\Skill;
use AppBundle\Controller\AbstractController;
use AppBundle\Form\SkillType;
/**
*Class SkillController.
*/
class SkillController extends AbstractController
{
/**
*function handler.
*/
protected function getHandler(){
//var_dump('test');
}
/**
*function addSkill
* #param Request $request
* #return \Symfony\Component\Form\Form The form
*/
public function addSkillAction(Request $request) {
$skill = $this->getHandler()->post();
if ($skill instanceof \AppBundle\Entity\Skill) {
return $this->redirectToRoute('ajouter_info');
}
return $this->render('skills/newskill.html.twig', array(
'form' => $form->createView(),));
}
}
Code SkillHandler.php:
<?php
namespace AppBundle\Handler;
use AppBundle\Handler\HandlerInterface;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Entity\Skill;
use Doctrine\ORM\EntityManager;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\Form\formFactory;
/**
* SkillHandler.
*/
class SkillHandler implements HandlerInterface {
/**
*
* #var EntityManager
*/
protected $em;
/**
*
* #var formFactory
*/
private $formFactory;
/**
*function construct.
*/
public function __construct(EntityManager $entityManager, formFactory $formFactory)
{
$this->em = $entityManager;
$this->formFactory = $formFactory;
}
/**
*function post
*/
public function post(array $parameters, array $options = []) {
$form = $this->formFactory->create(\AppBundle\Form\SkillType::class, $object, $options);
$form->submit($parameters);
if ($form->isValid()) {
$skill = $form->getData();
$this->persistAndFlush($skill);
return $skill;
}
return $form->getData();
}
/**
*function persisteAndFlush
*/
protected function persistAndFlush($object) {
$this->em->persist($object);
$this->em->flush();
}
/**
*function get
*/
public function get($id){
throw new \DomainException('Method SkillHandler::get not implemented');
}
/**
*function all
*/
public function all($limit = 10, $offset = 0){
throw new \DomainException('Method SkillHandler::all not implemented');
}
/**
*function put
*/
public function put($resource, array $parameters, array $options){
throw new \DomainException('Method SkillHandler::put not implemented');
}
/**
*function patch
*/
public function patch($resource, array $parameters, array $options){
throw new \DomainException('Method SkillHandler::patch not implemented');
}
/**
*function delete
*/
public function delete($resource){
throw new \DomainException('Method SkillHandler::delete not implemented');
}
}
code services.yml:
skill_add:
class: AppBundle\Handler\SkillHandler
arguments:
- "#doctrine.orm.entity_manager"
- "#form.factory"
public: true
Any help would be appreciated.
Your $this->getHandler() retruns null.
Solution can be checking if $this->getHandler() doesn't return null in first place.
if (!$this->getHandler()) {
throw new \Exception(sprintf('Handler cannot be null')
} else {
$skill = $this->getHandler()->post();
}
Try this, firstly you should take your handler into getHandler() method at your Controller.
protected function getHandler(){
return $this->get('skill_add');
}
I'm writting some tests for a web application, and one of them is Failing when in production & development is working fine.
That's the fail:
myMelomanBundle\Publication\CreatePublicationUseCaseTest::shouldCreateAPublicationOneTimeIfItDoesNotExist
TypeError: Argument 1 passed to myDomain\Entity\Publication::setUser() must be an instance of myDomain\Entity\User, null given, called in /var/www/melomaniacs/src/myDomain/UseCases/Publication/CreatePublicationUseCase.php on line 48
CreatePublicationUseCaseTest.php:
<?php
namespace myMelomanBundle\Publication;
use myDomain\Entity\Publication;
use myDomain\UseCases\Publication\CreatePublicationUseCase;
use myMelomanBundle\Repository\UserRepository;
use myMelomanBundle\Repository\PublicationRepository;
use PHPUnit_Framework_MockObject_MockObject;
use Doctrine\ORM\EntityManagerInterface;
use myDomain\Entity\User;
class CreatePublicationUseCaseTest extends \PHPUnit_Framework_TestCase
{
const USER_ID = 2;
const MESSAGE = "message";
const URI = "spotify:uri:47n4in3482nk";
/**
* #var CreatePublicationUseCase
*/
private $createPublicationUseCase;
/**
* #var PHPUnit_Framework_MockObject_MockObject
*/
private $userRepositoryMock;
/**
* #var PHPUnit_Framework_MockObject_MockObject
*/
private $publicationRepositoryMock;
/**
* #var PHPUnit_Framework_MockObject_MockObject
*/
private $aDispatcherMock;
/**
* #var PHPUnit_Framework_MockObject_MockObject
*/
private $aEntityMock;
/**
* #var PHPUnit_Framework_MockObject_MockObject
*/
private $userMock;
protected function setUp()
{
$this->userRepositoryMock = $this->createMock(UserRepository::class);
$this->publicationRepositoryMock = $this->createMock(PublicationRepository::class);
$this->aEntityMock = $this->createMock(EntityManagerInterface::class);
$this->createPublicationUseCase = new CreatePublicationUseCase($this->publicationRepositoryMock, $this->userRepositoryMock, $this->aEntityMock);
$this->userMock = $this->createMock(User::class);
}
protected function tearDown()
{
$this->userRepositoryMock = null;
$this->publicationRepositoryMock = null;
$this->createPublicationUseCase = null;
$this->userMock = null;
}
/** #test */
public function dummyTest()
{
$this->createPublicationUseCase;
}
/** #test */
public function shouldCreateAPublicationOneTimeIfItDoesNotExist()
{
$this->givenAPublicationRepositoryThatDoesNotHaveASpecificPublication();
$this->thenThePublicationShouldBeSavedOnce();
$this->whenTheCreateUserUseCaseIsExecutedWithASpecificParameters();
}
private function givenAPublicationRepositoryThatDoesNotHaveASpecificPublication()
{
$this->publicationRepositoryMock
->method('find')
->willReturn(false);
}
private function thenThePublicationShouldBeSavedOnce()
{
$this->publicationRepositoryMock
->expects($this->once())
->method('create')
->willReturn($this->isInstanceOf(Publication::class));
}
private function whenTheCreateUserUseCaseIsExecutedWithASpecificParameters()
{
$this->createPublicationUseCase->execute(self::USER_ID, self::MESSAGE, null);
}
}
CreatePublicationUseCase.php
<?php
namespace myDomain\UseCases\Publication;
use Doctrine\ORM\EntityManagerInterface;
use myDomain\Entity\Publication;
use myDomain\Entity\User;
use myDomain\PublicationRepositoryInterface;
use myDomain\UserRepositoryInterface;
use myMelomanBundle\Repository\PublicationRepository;
use myMelomanBundle\Repository\UserRepository;
class CreatePublicationUseCase
{
/**
* #var PublicationRepository
*/
private $publicationRepository;
/**
* #var UserRepository
*/
private $userRepository;
private $entityManager;
public function __construct(
PublicationRepositoryInterface $publicationRepository,
UserRepositoryInterface $userRepository,
EntityManagerInterface $entityManager
)
{
$this->publicationRepository = $publicationRepository;
$this->userRepository = $userRepository;
$this->entityManager = $entityManager;
}
public function execute($userId, $message = null, $uri = null)
{
try{
/**
* #var User $user
*/
$user = $this->userRepository->findOneBy(array('id'=>$userId));
\Doctrine\Common\Util\Debug::dump($user);die; => Here
$publication = new Publication();
$publication->setMessage($message == null ? '' : $message);
$publication->setCreatedAt(new \DateTime());
$publication->setUser($user);
$publication->setStatus(0);
$publication->setLink($uri == null ? '' : $uri);
$this->publicationRepository->create($publication);
$this->entityManager->flush();
return $publication;
} catch (\Exception $e) {
return false;
}
}
}
Note that where is the dump , it returns the user object properly, just on the Test it's returning NULL.
On the test, it should be getting to me same User object result that without the test, shouldn't be?
What I'm doing wrong?
It looks like your issue is coming from the following line from CreatePublicationUseCase::execute
$user = $this->userRepository->findOneBy(array('id'=>$userId));
In your test, you pass in a mocked UserRepository but you don't mock the output of findOneBy.
$this->userRepositoryMock = $this->createMock(UserRepository::class);
I believe you will have the results you want if you also mock the results with something like the following.
$this->userRepositoryMock
->expects($this->once())
->method('findOneBy')
->will($this->returnValue(<value>));
I'm on symfony 2.6.3 with stof Doctrine extension.
TimeStampable and SoftDeletable work well.
Also Blameable "on create" and "on update" are working well too:
/**
* #var User $createdBy
*
* #Gedmo\Blameable(on="create")
* #ORM\ManyToOne(targetEntity="my\TestBundle\Entity\User")
* #ORM\JoinColumn(name="createdBy", referencedColumnName="id")
*/
protected $createdBy;
/**
* #var User $updatedBy
*
* #Gedmo\Blameable(on="update")
* #ORM\ManyToOne(targetEntity="my\TestBundle\Entity\User")
* #ORM\JoinColumn(name="updatedBy", referencedColumnName="id")
*/
protected $updatedBy;
But "on change" seems not to be working.
/**
* #var User $deletedBy
*
* #Gedmo\Blameable(on="change", field="deletedAt")
* #ORM\ManyToOne(targetEntity="my\UserBundle\Entity\User")
* #ORM\JoinColumn(name="deletedBy", referencedColumnName="id")
*/
protected $deletedBy;
I've got SoftDeletable configured on "deletedAt" field. SoftDeletable works fine, but deletedBy is never filled.
How can I manage to make it work? I just want to set user id who deleted the entity.
Here my solution :
mybundle.soft_delete:
class: Listener\SoftDeleteListener
arguments:
- #security.token_storage
tags:
- { name: doctrine_mongodb.odm.event_listener, event: preSoftDelete }
class SoftDeleteListener
{
/**
* #var TokenStorageInterface
*/
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
/**
* Method called before "soft delete" system happened.
*
* #param LifecycleEventArgs $lifeCycleEvent Event details.
*/
public function preSoftDelete(LifecycleEventArgs $lifeCycleEvent)
{
$document = $lifeCycleEvent->getDocument();
if ($document instanceof SoftDeletedByInterface) {
$token = $this->tokenStorage->getToken();
if (is_object($token)) {
$oldValue = $document->getDeletedBy();
$user = $token->getUser();
$document->setDeletedBy($user);
$uow = $lifeCycleEvent->getObjectManager()->getUnitOfWork();
$uow->propertyChanged($document, 'deletedBy', $oldValue, $user);
$uow->scheduleExtraUpdate($document, array('deletedBy' => array($oldValue, $user)));
}
}
}
}
The problem is you want to update entity (set user) when you call remove method on it.
Currently there may not be a perfect solution for registering user who soft-deleted an object using Softdeleteable + Blameable extensions.
Some idea might be to overwrite SoftDeleteableListener (https://github.com/Atlantic18/DoctrineExtensions/blob/master/lib/Gedmo/SoftDeleteable/SoftDeleteableListener.php) but I had a problem doing it.
My current working solution is to use Entity Listener Resolver.
MyEntity.php
/**
* #ORM\EntityListeners({„Acme\MyBundle\Entity\Listener\MyEntityListener" })
*/
class MyEntity {
/**
* #ORM\ManyToOne(targetEntity="Acme\UserBundle\Entity\User")
* #ORM\JoinColumn(name="deleted_by", referencedColumnName="id")
*/
private $deletedBy;
public function getDeletedBy()
{
return $this->deletedBy;
}
public function setDeletedBy($deletedBy)
{
$this->deletedBy = $deletedBy;
}
MyEntityListener.php
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Acme\MyBundle\Entity\MyEntity;
class MyEntityListener
{
/**
* #var TokenStorageInterface
*/
private $token_storage;
public function __construct(TokenStorageInterface $token_storage)
{
$this->token_storage = $token_storage;
}
public function preRemove(MyEntity $myentity, LifecycleEventArgs $event)
{
$token = $this->token_storage->getToken();
if (null !== $token) {
$entityManager = $event->getObjectManager();
$myentity->setDeletedBy($token->getUser());
$entityManager->persist($myentity);
$entityManager->flush();
}
}
}
An imperfection here is calling flush method.
Register service:
services:
myentity.listener.resolver:
class: Acme\MyBundle\Entity\Listener\MyEntityListener
arguments:
- #security.token_storage
tags:
- { name: doctrine.orm.entity_listener, event: preRemove }
Update doctrine/doctrine-bundle in composer.json:
"doctrine/doctrine-bundle": "1.3.x-dev"
If you have any other solutions, especially if it is about SoftDeleteableListener, please post it here.
This is my solution, I use preSoftDelete event:
app.event.entity_delete:
class: AppBundle\EventListener\EntityDeleteListener
arguments:
- #security.token_storage
tags:
- { name: doctrine.event_listener, event: preSoftDelete, connection: default }
and service:
<?php
namespace AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class EntityDeleteListener
{
/**
* #var TokenStorageInterface
*/
private $tokenStorage;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function preSoftDelete(LifecycleEventArgs $args)
{
$token = $this->tokenStorage->getToken();
$object = $args->getEntity();
$om = $args->getEntityManager();
$uow = $om->getUnitOfWork();
if (!method_exists($object, 'setDeletedBy')) {
return;
}
if (null == $token) {
throw new AccessDeniedException('Only authorized users can delete entities');
}
$meta = $om->getClassMetadata(get_class($object));
$reflProp = $meta->getReflectionProperty('deletedBy');
$oldValue = $reflProp->getValue($object);
$reflProp->setValue($object, $token->getUser()->getUsername());
$om->persist($object);
$uow->propertyChanged($object, 'deletedBy', $oldValue, $token->getUser()->getUsername());
$uow->scheduleExtraUpdate($object, array(
'deletedBy' => array($oldValue, $token->getUser()->getUsername()),
));
}
}
It's not consistence because I check setDeletedBy method exists and set deletedBy property, but it work for me, and you can upgrade this code for your needs
Here is another solution I found :
Register a service:
softdeleteable.listener:
class: AppBundle\EventListener\SoftDeleteableListener
arguments:
- '#security.token_storage'
tags:
- { name: doctrine.event_listener, event: preFlush, method: preFlush }
SoftDeleteableListener:
/**
* #var TokenStorageInterface|null
*/
private $tokenStorage;
/**
* DoctrineListener constructor.
*
* #param TokenStorageInterface|null $tokenStorage
*/
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
/**
* #param PreFlushEventArgs $event
*/
public function preFlush(PreFlushEventArgs $event)
{
$user = $this->getUser();
$em = $event->getEntityManager();
foreach ($em->getUnitOfWork()->getScheduledEntityDeletions() as $object) {
/** #var SoftDeleteableEntity|BlameableEntity $object */
if (method_exists($object, 'getDeletedBy') && $user instanceof User) {
$object->setDeletedBy($user);
$em->merge($object);
// Persist and Flush allready managed by other doctrine extensions.
}
}
}
/**
* #return User|void
*/
public function getUser()
{
if (!$this->tokenStorage || !$this->tokenStorage instanceof TokenStorageInterface) {
throw new \LogicException('The SecurityBundle is not registered in your application.');
}
$token = $this->tokenStorage->getToken();
if (!$token) {
/** #noinspection PhpInconsistentReturnPointsInspection */
return;
}
$user = $token->getUser();
if (!$user instanceof User) {
/** #noinspection PhpInconsistentReturnPointsInspection */
return;
}
return $user;
}
I'm currently using Symfony2 to create (and learn how to) a REST API. I'm using FOSRestBundle and i've created an "ApiControllerBase.php" with the following :
<?php
namespace Utopya\UtopyaBundle\Controller;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\View\View;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class ApiControllerBase
*
* #package Utopya\UtopyaBundle\Controller
*/
abstract class ApiControllerBase extends FOSRestController
{
/**
* #param string $entityName
* #param string $entityClass
*
* #return array
* #throws NotFoundHttpException
*/
protected function getObjects($entityName, $entityClass)
{
$dataRepository = $this->container->get("doctrine")->getRepository($entityClass);
$entityName = $entityName."s";
$data = $dataRepository->findAll();
foreach ($data as $object) {
if (!$object instanceof $entityClass) {
throw new NotFoundHttpException("$entityName not found");
}
}
return array($entityName => $data);
}
/**
* #param string $entityName
* #param string $entityClass
* #param integer $id
*
* #return array
* #throws NotFoundHttpException
*/
protected function getObject($entityName, $entityClass, $id)
{
$dataRepository = $this->container->get("doctrine")->getRepository($entityClass);
$data = $dataRepository->find($id);
if (!$data instanceof $entityClass) {
throw new NotFoundHttpException("$entityName not found");
}
return array($entityClass => $data);
}
/**
* #param FormTypeInterface $objectForm
* #param mixed $object
* #param string $route
*
* #return Response
*/
protected function processForm(FormTypeInterface $objectForm, $object, $route)
{
$statusCode = $object->getId() ? 204 : 201;
$em = $this->getDoctrine()->getManager();
$form = $this->createForm($objectForm, $object);
$form->submit($this->container->get('request_stack')->getCurrentRequest());
if ($form->isValid()) {
$em->persist($object);
$em->flush();
$response = new Response();
$response->setStatusCode($statusCode);
// set the `Location` header only when creating new resources
if (201 === $statusCode) {
$response->headers->set('Location',
$this->generateUrl(
$route, array('id' => $object->getId(), '_format' => 'json'),
true // absolute
)
);
}
return $response;
}
return View::create($form, 400);
}
}
This handles getting one object with a given id, all objects and process a form. But to use this i have to create as many controller as needed. By example : GameController.
<?php
namespace Utopya\UtopyaBundle\Controller;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\View\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Utopya\UtopyaBundle\Entity\Game;
use Utopya\UtopyaBundle\Form\GameType;
/**
* Class GameController
*
* #package Utopya\UtopyaBundle\Controller
*/
class GameController extends ApiControllerBase
{
private $entityName = "Game";
private $entityClass = 'Utopya\UtopyaBundle\Entity\Game';
/**
* #Rest\View()
*/
public function getGamesAction()
{
return $this->getObjects($this->entityName, $this->entityClass);
}
/**
* #param int $id
*
* #return array
* #throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* #Rest\View()
*/
public function getGameAction($id)
{
return $this->getObject($this->entityName, $this->entityClass, $id);
}
/**
* #return mixed
*/
public function postGameAction()
{
return $this->processForm(new GameType(), new Game(), "get_game");
}
}
This sound not so bad to me but there's a main problem : if i want to create another controller (by example Server or User or Character), i'll have to do the same process and i don't want to since it'll be the same logic.
Another "maybe" problem could be my $entityName and $entityClass.
Any idea or could i make this better ?
Thank-you !
===== Edit 1 =====
I think i made up my mind. For those basics controllers. I would like to be able to "configure" instead of "repeat".
By example i could make a new node in config.yml with the following :
#config.yml
mynode:
game:
entity: 'Utopya\UtopyaBundle\Entity\Game'
This is a very basic example but is it possible to make this and transform it into my GameController with 3 methods routes (getGame, getGames, postGame) ?
I just want some leads if i can really achieve with this way or not, if yes with what components ? (Config, Router, etc.)
If no, what could i do? :)
Thanks !
I'd like to show my approach here.
I've created a base controller for the API, and I stick with the routing that's generated with FOSRest's rest routing type. Hence, the controller looks like this:
<?php
use FOS\RestBundle\Controller\Annotations\View;
use \Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Psr\Log\LoggerInterface;
use Symfony\Component\Form\FormFactoryInterface,
Symfony\Bridge\Doctrine\RegistryInterface,
Symfony\Component\Security\Core\SecurityContextInterface,
Doctrine\Common\Persistence\ObjectRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
abstract class AbstractRestController
{
/**
* #var \Symfony\Component\Form\FormFactoryInterface
*/
protected $formFactory;
/**
* #var string
*/
protected $formType;
/**
* #var string
*/
protected $entityClass;
/**
* #var SecurityContextInterface
*/
protected $securityContext;
/**
* #var RegistryInterface
*/
protected $doctrine;
/**
* #var EventDispatcherInterface
*/
protected $dispatcher;
/**
* #param FormFactoryInterface $formFactory
* #param RegistryInterface $doctrine
* #param SecurityContextInterface $securityContext
* #param LoggerInterface $logger
*/
public function __construct(
FormFactoryInterface $formFactory,
RegistryInterface $doctrine,
SecurityContextInterface $securityContext,
EventDispatcherInterface $dispatcher
)
{
$this->formFactory = $formFactory;
$this->doctrine = $doctrine;
$this->securityContext = $securityContext;
$this->dispatcher = $dispatcher;
}
/**
* #param string $formType
*/
public function setFormType($formType)
{
$this->formType = $formType;
}
/**
* #param string $entityClass
*/
public function setEntityClass($entityClass)
{
$this->entityClass = $entityClass;
}
/**
* #param null $data
* #param array $options
*
* #return \Symfony\Component\Form\FormInterface
*/
public function createForm($data = null, $options = array())
{
return $this->formFactory->create(new $this->formType(), $data, $options);
}
/**
* #return RegistryInterface
*/
public function getDoctrine()
{
return $this->doctrine;
}
/**
* #return \Doctrine\ORM\EntityRepository
*/
public function getRepository()
{
return $this->doctrine->getRepository($this->entityClass);
}
/**
* #param Request $request
*
* #View(serializerGroups={"list"}, serializerEnableMaxDepthChecks=true)
*/
public function cgetAction(Request $request)
{
$this->logger->log('DEBUG', 'CGET ' . $this->entityClass);
$offset = null;
$limit = null;
if ($range = $request->headers->get('Range')) {
list($offset, $limit) = explode(',', $range);
}
return $this->getRepository()->findBy(
[],
null,
$limit,
$offset
);
}
/**
* #param int $id
*
* #return object
*
* #View(serializerGroups={"show"}, serializerEnableMaxDepthChecks=true)
*/
public function getAction($id)
{
$this->logger->log('DEBUG', 'GET ' . $this->entityClass);
$object = $this->getRepository()->find($id);
if (!$object) {
throw new NotFoundHttpException(sprintf('%s#%s not found', $this->entityClass, $id));
}
return $object;
}
/**
* #param Request $request
*
* #return \Symfony\Component\Form\Form|\Symfony\Component\Form\FormInterface
*
* #View()
*/
public function postAction(Request $request)
{
$object = new $this->entityClass();
$form = $this->createForm($object);
$form->submit($request);
if ($form->isValid()) {
$this->doctrine->getManager()->persist($object);
$this->doctrine->getManager()->flush($object);
return $object;
}
return $form;
}
/**
* #param Request $request
* #param int $id
*
* #return \Symfony\Component\Form\FormInterface
*
* #View()
*/
public function putAction(Request $request, $id)
{
$object = $this->getRepository()->find($id);
if (!$object) {
throw new NotFoundHttpException(sprintf('%s#%s not found', $this->entityClass, $id));
}
$form = $this->createForm($object);
$form->submit($request);
if ($form->isValid()) {
$this->doctrine->getManager()->persist($object);
$this->doctrine->getManager()->flush($object);
return $object;
}
return $form;
}
/**
* #param Request $request
* #param $id
*
* #View()
*
* #return object|\Symfony\Component\Form\FormInterface
*/
public function patchAction(Request $request, $id)
{
$this->logger->log('DEBUG', 'PATCH ' . $this->entityClass);
$object = $this->getRepository()->find($id);
if (!$object) {
throw new NotFoundHttpException(sprintf('%s#%s not found', $this->entityClass, $id));
}
$form = $this->createForm($object);
$form->submit($request, false);
if ($form->isValid()) {
$this->doctrine->getManager()->persist($object);
$this->doctrine->getManager()->flush($object);
return $object;
}
return $form;
}
/**
* #param int $id
*
* #return array
*
* #View()
*/
public function deleteAction($id)
{
$this->logger->log('DEBUG', 'DELETE ' . $this->entityClass);
$object = $this->getRepository()->find($id);
if (!$object) {
throw new NotFoundHttpException(sprintf('%s#%s not found', $this->entityClass, $id));
}
$this->doctrine->getManager()->remove($object);
$this->doctrine->getManager()->flush($object);
return ['success' => true];
}
}
It has methods for most of RESTful actions. Next, when I want to implement a CRUD for an entity, that's what I do:
The abstract controller is registered in the DI as an abstract service:
my_api.abstract_controller:
class: AbstractRestController
abstract: true
arguments:
- #form.factory
- #doctrine
- #security.context
- #logger
- #event_dispatcher
Next, when I'm implementing a CRUD for a new entity, I create an empty class and a service definition for it:
class SettingController extends AbstractRestController implements ClassResourceInterface {}
Note that the class implements ClassResourceInterface. This is necessary for the rest routing to work.
Here's the service declaration for this controller:
api.settings.controller.class: MyBundle\Controller\SettingController
api.settings.form.class: MyBundle\Form\SettingType
my_api.settings.controller:
class: %api.settings.controller.class%
parent: my_api.abstract_controller
calls:
- [ setEntityClass, [ MyBundle\Entity\Setting ] ]
- [ setFormType, [ %my_api.settings.form.class% ] ]
Then, I'm just including the controller in routing.yml as the FOSRestBundle doc states, and it's done.