Persisting Updated Objects in Symfony - php

I have a symfony application where I am attempting to update an entity in the database using a setter. However when I update the entity and call $this->em->flush() the entity is not persisted to the database.
Here is my service:
<?php
namespace AppBundle\Service;
use AppBundle\Exceptions\UserNotFoundException;
use Doctrine\ORM\EntityManager;
use AppBundle\Entity\User;
/**
* Class MyService
* #package AppBundle\Service
*/
class MyService extends BaseService {
/**
* #var EntityManager
*/
protected $em;
/**
* #var User
*/
protected $user;
/**
* MyService constructor.
* #param EntityManager $em
*/
public function __construct(EntityManager $em){
$this->em = $em;
}
/**
* See if a user email exists
* #param string $email
* #return bool
*/
public function checkEmailExists($email){
$this->user = $this->em
->getRepository('AppBundle:User')
->findOneBy(['email' => $email]);
return !(is_null($this->user));
}
/**
* add credit to a users account
* #param User $user
* #param integer $credit
*/
public function addCredit(User $user, $credit){
$user->addCredit($credit);
$this->em->flush();
}
/**
* add a credit to a users account
* #param $email
* #param $credit
*/
public function addCreditByEmail($email, $credit){
if(!($this->checkEmailExists($email))){
throw new UserNotFoundException(sprintf('User with email %s not found.', $email));
}
$this->addCredit($this->user, $credit);
}
}
Here is my test:
<?php
namespace AppBundle\Tests\Service;
use AppBundle\DataFixtures\ORM\PurchaseFixture;
use AppBundle\Entity\Vendor;
use AppBundle\Repository\OfferRepository;
use AppBundle\Tests\TestCase;
use AppBundle\Entity\Offer;
use AppBundle\DataFixtures\ORM\OfferFixture;
use AppBundle\DataFixtures\ORM\PaymentSystemFixture;
/**
* Class UserOfferServiceTest
* #package AppBundle\Tests\Service
*/
class MyServiceTest extends TestCase implements ServiceTestInterface
{
function __construct($name = null, array $data = [], $dataName = '')
{
$this->setFixtures([
'AppBundle\DataFixtures\ORM\CityFixture',
'AppBundle\DataFixtures\ORM\CountryFixture',
'AppBundle\DataFixtures\ORM\PaymentSystemFixture',
'AppBundle\DAtaFixtures\ORM\UserFixture',
]);
parent::__construct($name, $data, $dataName);
}
/**
* test the checkEmailExists() of app.vendor
*/
public function testCheckEmailExists(){
$myService = $this->getService();
$this->assertTrue($myService->checkEmailExists('user1#user1.com'));
$this->assertFalse($myService->checkEmailExists($this->fake()->safeEmail));
}
/**
* test the addCredit functionality
*/
public function testAddCredit(){
$myService = $this->getService();
$user = $this->getUser();
$this->assertEquals(0, $user->getCredit());
$toAdd = $this->fake()->numberBetween(1, 500);
$myService->addCredit($user, $toAdd);
$this->assertEquals($toAdd, $user->getCredit());
}
/**
* test the addCreditByEmail functionality
*/
public function testAddCreditByEmail(){
$myService = $this->getService();
$user = $this->getUser();
$email = $this->getUser()->getEmail();
$this->assertEquals(0, $user->getCredit());
$toAdd = $this->fake()->numberBetween(1, 500);
$myService->addCreditByEmail($email, $toAdd);
$updatedUser = $this->getEntityManager()
->getRepository('AppBundle:User')
->findOneBy(['email' => $email]);
$this->assertEquals($toAdd, $updatedUser->getCredit());
}
/**
* #return \AppBundle\Service\VendorService|object
*/
public function getService(){
$this->seedDatabase();
$this->client = $this->createClient();
return $this->client->getContainer()->get('app.admin_kiosk');
}
}
The testAddCredit() test passes (because I'm checking the same object), but the testAddCreditByEmail fails with the following error: 1) AppBundle\Tests\Service\MyServiceTest::testAddCreditByEmail
Failed asserting that null matches expected 149.
I've tried persisting the entity, flushing the entity (like: $this->em->flush($user)) all to no avail. Please let me know how I can fix this.

I found the issue.
In the test case I just had to refresh the entity by doing $this->getEntityManager()->refresh($user) (on the original $user entity). Thanks!

Related

Call to a member function has() on null

im learning symfony. I have a twig using twig extension and i found that i cannot call a function from other controller inside twig extension.
so i have decide to use a service since the twig extension is using the service.
so i tried to call a function from controller in the service and call the function from the service in the twig extension.
Then, i have an error message that [Call to a member function has() on null], where $doct = $this->getDoctrine()->getManager(); in the controller.
this is the function in the controller that i'm trying to use
edited
namespace Customize\Controller;
use Customize\Entity\CustomRing;
use Customize\Entity\Ring;
use Customize\Repository\CustomProductRepository;
use Eccube\Controller\AbstractController;
use Eccube\Entity\BaseInfo;
use Eccube\Entity\Product;
use Eccube\Entity\ProductClass;
use Eccube\Event\EccubeEvents;
use Eccube\Event\EventArgs;
use Eccube\Repository\BaseInfoRepository;
use Eccube\Repository\ProductClassRepository;
use Eccube\Service\CartService;
use Eccube\Service\OrderHelper;
use Eccube\Service\PurchaseFlow\PurchaseContext;
use Eccube\Service\PurchaseFlow\PurchaseFlow;
use Eccube\Service\PurchaseFlow\PurchaseFlowResult;
use Google\Service\Monitoring\Custom;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class CartController extends AbstractController
{
/**
* #var ProductClassRepository
*/
protected $productClassRepository;
/**
* #var CartService
*/
protected $cartService;
/**
* #var PurchaseFlow
*/
protected $purchaseFlow;
/**
* #var BaseInfo
*/
protected $baseInfo;
/**
* #var CustomProductRepository
*/
protected $customProductRepository;
/**
* CartController constructor.
*
* #param ProductClassRepository $productClassRepository
* #param CartService $cartService
* #param PurchaseFlow $cartPurchaseFlow
* #param BaseInfoRepository $baseInfoRepository
* #param CustomProductRepository $customProductRepository
*/
public function __construct(
ProductClassRepository $productClassRepository,
CartService $cartService,
PurchaseFlow $cartPurchaseFlow,
BaseInfoRepository $baseInfoRepository,
CustomProductRepository $customProductRepository
) {
$this->productClassRepository = $productClassRepository;
$this->cartService = $cartService;
$this->purchaseFlow = $cartPurchaseFlow;
$this->baseInfo = $baseInfoRepository->get();
$this->customProductRepository = $customProductRepository;
}
public function image_route(){
$Carts = $this->cartService->getCarts();
$doct = $this->getDoctrine()->getManager();
$custom_images = array();
$Custom_product = $this->customProductRepository->customProductFindByName();
$Product = $doct->getRepository(Product::class)->find($Custom_product[0]->getId());
foreach ($Carts as $Cart) {
$items = $Cart->getCartItems();
foreach($items as $item ){
if($item->getProductClass()->getProduct()->getId() == $Product->getId()){
$custom = $item->getProductClass()->getCode();
$custom = $doct->getRepository(CustomRing::class)->find($custom);
$ring = $doct->getRepository(Ring::class)->find($custom->getRingBaseId());
$upload_directory= $this->getParameter('uploads_directory');
$ring_shape = $ring->getRingShape();
$ring_type = $ring->getRingType();
$upload = $upload_directory.'/customRing/ring/'.$ring_shape.'/'.$ring_type.'/';
$images = glob($upload."*.{jpg,png,jpeg,JPG,JPEG,PNG}", GLOB_BRACE);
for($i=0;$i<count($images);$i++){
$aa = explode('save_image/', $images[$i]);
$images[$i] = $aa[1];
}
array_push($custom_images,$images[0]);
}
}
}
return $custom_images;
use Customize\Controller\CartController;
/**
* #var CartController
*/
protected $cartController;
public function __construct(
SessionInterface $session,
EntityManagerInterface $entityManager,
ProductClassRepository $productClassRepository,
CartRepository $cartRepository,
CartItemComparator $cartItemComparator,
CartItemAllocator $cartItemAllocator,
OrderRepository $orderRepository,
TokenStorageInterface $tokenStorage,
AuthorizationCheckerInterface $authorizationChecker,
CartController $CartController
) {
$this->session = $session;
$this->entityManager = $entityManager;
$this->productClassRepository = $productClassRepository;
$this->cartRepository = $cartRepository;
$this->cartItemComparator = $cartItemComparator;
$this->cartItemAllocator = $cartItemAllocator;
$this->orderRepository = $orderRepository;
$this->tokenStorage = $tokenStorage;
$this->authorizationChecker = $authorizationChecker;
$this->cartController = $CartController;
}
/*other functions
.
.
*/
public function imagess(){
$temp = $this->cartController->image_route();
return $temp;
}
and this is the twig extension
use Eccube\Service\CartService;
class CartServiceExtension extends AbstractExtension
{
/**
* #var CartService
*/
protected $cartService;
public function get_image_route(){
$t = $this->cartService->imagess();
return $t;
//return ['11', '12'];
}
}
The line i got error in controller is
$doct = $this->getDoctrine()->getManager();
and when go inside getDoctrine()
trait ControllerTrait{
/**
* Shortcut to return the Doctrine Registry service.
*
* #return ManagerRegistry
*
* #throws \LogicException If DoctrineBundle is not available
*
* #final
*/
protected function getDoctrine()
{
if (!$this->container->has('doctrine')) {
throw new \LogicException('The DoctrineBundle is not registered in your application. Try running "composer require symfony/orm-pack".');
}
return $this->container->get('doctrine');
}
I don't know why but it seems has() function inside getDoctrine() returns null
how can i use the function from the controller so that i can pass the values of $custom_images into the twig?
To get Doctrine in any controller action, you can do away with that function/trait entirely and just inject the EntityManagerInterface into your action.
For example:
class SomeCustomController extends AbstractController
{
public function someControllerAction(Request $request, EntityManagerInterface $em): Response
{
// business logic
}
}

TearDown database after a phpUnitTest on a WebTestCase using DataFixtures

I have the following phpUnit functional Test:
namespace Tests\AppBundle\Controller;
/**
* #testtype Functional
*/
class DefaultControllerTest extends BasicHttpController
{
/**
* {#inheritdoc}
*/
public function setUp()
{
$client = static::createClient();
$container = $client->getContainer();
$doctrine = $container->get('doctrine');
$entityManager = $doctrine->getManager();
$fixture = new YourFixture();
$fixture->load($entityManager);
}
/**
* {#inheritdoc}
*/
public function tearDown()
{
//Database is being destroyed here....
}
public function testIndex()
{
$client = static::createClient();
$crawler = $client->request('GET', '/');
$response=$client->getResponse();
$this->assertEquals(302, $response->getStatusCode());
$this->assertEquals('/login',$response->headers->get('Location'));
//#todo Create Dummy Users
$this->checkPanelAfterSucessfullLogin($crawler); //How I can create some user?
}
}
As you can see I load the following Fixture:
namespace AppBundle\DataFixtures\Test;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Doctrine\Common\Persistence\ObjectManager;
class DummyUserFixtures extends AbstractFixture implements OrderedFixtureInterface, ContainerAwareInterface
{
/**
* #var ContainerInterface
*/
private $container=null;
/**
* Generic function that creates a user with provided information.
* #param $name {String} The user's name
* #param $surname {String} The user's surname
* #param $username {String} The user's username
* #param $password {String} The user's password
* #param $email {String} The user's recovery email
* #param $role {String} The user's system role
* #param $phone {String | null} The user's phone number
* #param $organization {String|null} The user's organization
* #param $occupation {String|null} The user's occupation
*
* #return AppBundle\Entity\User
*/
private function createUser($name,$surname,$username,$password,$email,$role,$phone=null,$organization=null,$occupation=null)
{
$fosUserManager=$this->container->get('fos_user.user_manager');
/**
* #var AppBundle\Entity\User
*/
$user=$fosUserManager->createUser();
$user->setUsername($username);
$user->setEmail($email);
$user->setPlainPassword($password);
$user->setEnabled(true);
$user->setRoles(array($role));
$user->setName($name);
$user->setSurname($surname);
if($phone){
$user->setPhone($phone);
}
if($organization){
$user->setOrganization($organization);
}
if($occupation){
$user->serOccupation($occupation);
}
$fosUserManager->updateUser($user, true);
return $user;
}
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
/**
* {#inheritDoc}
*/
public function load(ObjectManager $manager)
{
$this->createUser('John','Doe','jdoe','simplepasswd','jdoe#example.com','ROLE_USER','+3021456742324','Acme Products','Soft Engineer');
$this->createUser('Jackie','Chan','jchan','thesimplepasswd','jackiechan#example.com','ROLE_ADMIN','+302141232324','Holywood','Actor');
$this->createUser('Chuck','Norris','chuck_norris','unhackablepasswd','chucknorris#example.com','ROLE_SUPERADMIN',null,'Universe','Master');
}
}
But I want to be able after my database changes happen to be able to tear down completely the database and recreate it for the next test. Do you have ant idea how to colpletely tear down the database?
As you are using Doctrine DataFixtures, you can implement this logic in your test:
<?php
namespace Tests\AppBundle\Controller;
Use Doctrine\Common\DataFixtures\Purger\ORMPurger;
/**
* #testtype Functional
*/
class DefaultControllerTest extends BasicHttpController
{
private $em;
/**
* {#inheritdoc}
*/
public function setUp()
{
$client = static::createClient();
$container = $client->getContainer();
$doctrine = $container->get('doctrine');
$this->em = $doctrine->getManager();
$fixture = new YourFixture();
$fixture->load($entityManager);
}
private function truncateEntities()
{
$purger = new ORMPurger($this->em);
$purger->purge();
}
public function tearDown()
{
$this->truncateEntities();
}
// your tests
}

Symfony 3.4 cannot load fixtures on Test: Error: Call to a member function get() on null

I have the following phpunit test for testing the controllers:
The DefaultControllerTest:
namespace Tests\AppBundle\Controller;
use Tests\AppBundle\Controller\BasicHttpController;
use AppBundle\DataFixtures\Test\DummyUserFixtures;
/**
* #testtype Functional
*/
class DefaultControllerTest extends BasicHttpController
{
/**
* {#inheritdoc}
*/
public function setUp()
{
$fixture = new DummyUserFixtures();
$fixture->load($this->entityManager);
}
/**
* Testing the Behavior when visiting the index page
*/
public function testIndex()
{
$client = $this->client;
$router=$client->getContainer()->get('router');
$crawler = $client->request('GET', '/');
$response=$client->getResponse();
$this->assertTrue($client->getResponse()->isRedirect());
$this->assertEquals($router->getRouteCollection()->get('fos_user_security_login')->getPath(),$response->headers->get('Location'));
//#todo Create Dummy Users
// $this->checkPanelAfterSucessfullLogin($crawler);
}
}
That extends the following test BasicHttpController (try to apply the DRY principle):
namespace Tests\AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
class BasicHttpController extends WebTestCase
{
protected $entityManager=null;
protected $client=null;
/**
* {#inheritdoc}
*/
public function __construct()
{
parent::__construct();
$this->client = static::createClient();
$container = $this->client->getContainer();
$doctrine = $container->get('doctrine');
$this->entityManager=$doctrine->getManager();
}
/**
* Remove all entities from the database
*/
protected function truncateEntities()
{
$purger = new ORMPurger($this->entityManager());
$purger->purge();
}
/**
* {#inheritdoc}
*/
public function tearDown()
{
$this->truncateEntities();
}
/**
* #param username String the user's username
* #param passwoρd String the user's password
*/
protected function checkPanelAfterSucessfullLogin($crawler,string $username,string $password)
{
//Submitting the form
$form=$crawler->selectButton('_submit')->form();
$form['_username']=$username;
$form['_password']=$password;
$crawler=$crawler->submit($form);
$response=$client->getResponse();
$this->assertTrue($client->getResponse()->isRedirect());
$client->followRedirect();
//Checking header
$headerDom=$crawler->filter('header')->childen()->filter('nav.navbar')->children();
$this->assertCount(1,$headerDom->find('a.navbar-brand')); //homepage link
$this->assertCount(1,$headerDom->find('a.btn-danger')); //Logout button
}
}
As you can see I try to load the following fixture:
namespace AppBundle\DataFixtures\Test;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Doctrine\Common\Persistence\ObjectManager;
class DummyUserFixtures extends AbstractFixture implements OrderedFixtureInterface,ContainerAwareInterface
{
/**
* #var ContainerInterface
*/
private $container=null;
/**
* {#inheritDoc}
*/
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
/**
* Generic function that creates a user with provided information.
* #param $name {String} The user's name
* #param $surname {String} The user's surname
* #param $username {String} The user's username
* #param $password {String} The user's password
* #param $email {String} The user's recovery email
* #param $role {String} The user's system role
* #param $phone {String | null} The user's phone number
* #param $organization {String|null} The user's organization
* #param $occupation {String|null} The user's occupation
*
* #return AppBundle\Entity\User
*/
private function createUser($name,$surname,$username,$password,$email,$role,$phone=null,$organization=null,$occupation=null)
{
$fosUserManager=$this->container->get('fos_user.user_manager');
/**
* #var AppBundle\Entity\User
*/
$user=$fosUserManager->createUser();
$user->setUsername($username);
$user->setEmail($email);
$user->setPlainPassword($password);
$user->setEnabled(true);
$user->setRoles(array($role));
$user->setName($name);
$user->setSurname($surname);
if($phone){
$user->setPhone($phone);
}
if($organization){
$user->setOrganization($organization);
}
if($occupation){
$user->setOccupation($occupation);
}
$fosUserManager->updateUser($user, true);
return $user;
}
/**
* {#inheritDoc}
*/
public function load(ObjectManager $manager)
{
$this->createUser('John','Doe','jdoe','simplepasswd','jdoe#example.com','ROLE_USER','+3021456742324','Acme Products','Soft Engineer');
$this->createUser('Jackie','Chan','jchan','thesimplepasswd','jackiechan#example.com','ROLE_ADMIN','+302141232324','Holywood','Actor');
$this->createUser('Chuck','Norris','chuck_norris','unhackablepasswd','chucknorris#example.com','ROLE_SUPERADMIN',null,'Universe','Master');
}
public function getOrder()
{
return 1;
}
}
But for some reason I get the following error:
There was 1 error:
1) Tests\AppBundle\Controller\DefaultControllerTest::testIndex
Error: Call to a member function get() on null
/home/vagrant/code/src/AppBundle/DataFixtures/Test/DummyUserFixtures.php:50
/home/vagrant/code/src/AppBundle/DataFixtures/Test/DummyUserFixtures.php:87
/home/vagrant/code/tests/AppBundle/Controller/DefaultControllerTest.php:19
Further debugging has proved that the error is triggered by the following line in DummyUserFixtures:
$fosUserManager=$this->container->get('fos_user.user_manager');
So do you know how to load the data via fixtures?
In order to get it working you should set the service container you generate from the static::createClient() method and pass it via the $fixture->setContainer($container)
So a good approach is to define the container as protected instance variable to the BasicHttpController so any Test class (eg. the DefaultControllerTest in your case) is able to load the fixtures accordingly.
So using the setUp method and instance variables of BasicHttpController should be the following:
//Namespace declaration goes there
class BasicHttpController extends WebTestCase
{
protected $entityManager=null;
protected $client=null;
protected $container=null;
/**
* {#inheritdoc}
*/
public function setUp()
{
$this->client = static::createClient();
$this->container = $this->client->getContainer();
$doctrine = $this->container->get('doctrine');
$this->entityManager=$doctrine->getManager();
}
// Rest methods here
}
Note: on classes that are getting inherited from BasicHttpController you can define the setUp like that:
public function setUp()
{
parent::setUp();
// Add extra stuff here
}
So you can do more setUp bootstrapping before tests.

Wrong instance passed in Laravel DB post request

So I am working on a page in Laravel that generates invite codes upon email submission. I have run into this issue, every time when I enter my email into the form, it is supposed to generate an invite code an input it into my DB then redirect me. Instead I get this error code:
Argument 1 passed to myapp\Repositories\Invite\EloquentInviteRepository::__construct()
must be an instance of Illuminate\Database\Eloquent\Model, instance of
Illuminate\Foundation\Application given, called in /var/www/laravel/bootstrap/compiled.php
on line 4259 and defined
This is my EloquentInviteRepository.php file, apparently line 21 is the line in error:
<?php namespace myapp\Repositories\Invite;
use myapp\Repositories\Crudable;
use Illuminate\Support\MessageBag;
use myapp\Repositories\Repository;
use Illuminate\Database\Eloquent\Model;
use myapp\Repositories\AbstractRepository;
class EloquentInviteRepository extends AbstractRepository implements Repository, Crudable, InviteRepository {
/**
* #var Illuminate\Database\Eloquent\Model
*/
protected $model;
/**
* Construct
*
* #param Illuminate\Database\Eloquent\Model $user
*/
public function __construct(Model $model)
{
parent::__construct(new MessageBag);
$this->model = $model;
}
/**
* Find a valid invite by a code
*
* #param string $code
* #return Illuminate\Database\Eloquent\Model
*/
public function getValidInviteByCode($code)
{
return $this->model->where('code', '=', $code)
->where('claimed_at', '=', null)
->first();
}
/**
* Create
*
* #param array $data
* #return Illuminate\Database\Eloquent\Model
*/
public function create(array $data)
{
$data['code'] = bin2hex(openssl_random_pseudo_bytes(16));
return $this->model->create($data);
}
/**
* Update
*
* #param array $data
* #return Illuminate\Database\Eloquent\Model
*/
public function update(array $data){}
/**
* Delete
*
* #param int $id
* #return boolean
*/
public function delete($id){}
}
In case anyone was curious; the __construct() interface from Illuminate\Database\Eloquent\Model.php:
/**
* Create a new Eloquent model instance.
*
* #param array $attributes
* #return void
*/
public function __construct(array $attributes = array())
{
$this->bootIfNotBooted();
$this->syncOriginal();
$this->fill($attributes);
}
and Illuminate\Foundation\Application.php:
/**
* Create a new Illuminate application instance.
*
* #param \Illuminate\Http\Request $request
* #return void
*/
public function __construct(Request $request = null)
{
$this->registerBaseBindings($request ?: $this->createNewRequest());
$this->registerBaseServiceProviders();
$this->registerBaseMiddlewares();
}
In case these help in debugging the issue!
As per request I have included my controller element used during the post function, this is the part that seems to activate the repository and prompt the error:
<?php
use myapp\Repositories\Invite\InviteRepository;
class InviteController extends BaseController {
/**
* InviteRepository
*
* #var myapp\Repositories\Invite\InviteRepository
*/
protected $repository;
/**
* Create a new instance of the InviteController
*
* #param myapp\Repositories\Invite\InviteRepository
*/
public function __construct(InviteRepository $repository)
{
$this->repository = $repository;
}
/**
* Create a new invite
*
* #return Response
*/
public function store()
{
$invite = $this->repository->create(Input::all());
}
}
RepositoryServiceProvider.php
<?php namespace myapp\Repositories;
use Illuminate\Support\ServiceProvider;
class RepositoryServiceProvider extends ServiceProvider {
/**
* Register
*/
public function register()
{
$this->registerInviteRepository();
}
/**
* Register the Invite Repository
*
* #return void
*/
public function registerInviteRepository()
{
$this->app->bind('myapp\Repositories\Invite\InviteRepository', function($app)
{
return new EloquentInviteRepository( new Invite );
});
}
}
Any idea's as to what I am missing?
Thanks for the help guys,
You've been a great resource so far!
In the file RepositoryServiceProvider.php, replace this
$this->app->bind('myapp\Repositories\Invite\InviteRepository', function($app)
{
return new EloquentInviteRepository( new Invite );
});
With this:
$this->app->bind('myapp\Repositories\Invite\InviteRepository',
'myapp\Repositories\Invite\EloquentInviteRepository');

Symfony 2 - keeping the code DRY - FOSRestBundle

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.

Categories