I'm still very new to symfony and really enjoying it.
I'm at the stage where I've managed to create and setup a service, the service itself uses 2 dependencies:
A Data API that returns json data (this is a separate library which
i have implemented as a service and came with its own unit tests).
The Doctrine Entity Manager.
The service pulls the data required using the api and then loops through the data and checks to see if the data already exists, if it does it updates the existing entity and persists it, Otherwise it creates a new entity assigns the data and persists it.
I now need to write a unit test for this, i've not used PHPUnit only from symfony2 tutorials which were testing responses from a controller.
How do i go about writing a unit test for this service?
In particular mocking the data that i would normally pull from the api.
and then checks to see if the entry needs to be updated or created?
A code example would be really helpful so i can then use this as a template to create tests for other similar services that i create.
Here's the Service i want to test:
<?php
namespace FantasyPro\DataBundle\DataManager;
use Doctrine\ORM\EntityManager;
use FantasyDataAPI\Client;
use FantasyPro\DataBundle\Entity\Stadium;
class StadiumParser {
/**
* #var EntityManager $em
*/
private $em;
/**
* #var Client $client
*/
private $client;
public function __construct( EntityManager $em, Client $client) {
$this->em = $em;
$this->client = $client;
}
/**
* #return array
*/
public Function parseData(){
//var_dump($this);
$stadiumData = $this->client->Stadiums();
//var_dump($stadiumData);
//get the Repo
$repo = $this->em->getRepository('DataBundle:Stadium');
$log = array();
foreach ($stadiumData as $stadium) {
// Get the current stadium in the list from the database
$criteria = array( 'stadiumID' => $stadium['StadiumID'] );
$currentStadium = $repo->FindOneBy( $criteria );
if ( ! $currentStadium) {
$currentStadium = new Stadium(); //no stadium with the StadiumID exists so create a new stadium
$logData = [
'action' => 'Added Stadium',
'itemID' => $stadium['StadiumID'],
'itemName' => $stadium['Name']
];
$log[] = $logData;
} else {
$logData = [
'action' => 'Updated Stadium',
'itemID' => $stadium['StadiumID'],
'itemName' => $stadium['Name']
];
$log[] = $logData;
}
$currentStadium->setStadiumID( $stadium['StadiumID'] );
$currentStadium->setName( $stadium['Name'] );
$currentStadium->setCity( $stadium['City'] );
$currentStadium->setState( $stadium['State'] );
$currentStadium->setCountry( $stadium['Country'] );
$currentStadium->setCapacity( $stadium['Capacity'] );
$currentStadium->setPlayingSurface( $stadium['PlayingSurface'] );
$this->em->persist( $currentStadium );
}
$this->em->flush();
return $log;
}
}
****** UPDATE *******
after reading ilpaijin's answer.
I've simplified the service so it no longer returns a log, i initially had this in so i could check what had been added by sending the log to a twig template in my controller, I eventually plan to have this running as a command so i can run a it via a cron job so the log bit is unnecessary.
I'm now setting the entity within my construct as i couldn't work out how to pass an entity as a injected dependency.
Now grabbing a new entity by using createNewStadium() method.
The Updated Service:
namespace FantasyPro\DataBundle\DataManager;
use Doctrine\ORM\EntityManager;
use FantasyDataAPI\Client;
use FantasyPro\DataBundle\Entity\Stadium;
class StadiumParser {
/**
* #var EntityManager $em
*/
private $em;
/**
* #var Client $client
*/
private $client;
/**
* #var Stadium Stadium
*/
private $stadium;
public function __construct( EntityManager $em, Client $client) {
$this->em = $em;
$this->client = $client;
}
/**
* Gets a list of stadiums using $this->client->Stadiums.
* loops through returned stadiums and persists them
* when loop has finished flush them to the db
*/
public Function parseData(){
$data = $this->client->Stadiums();
//get the Repo
$repo = $this->em->getRepository('DataBundle:Stadium');
foreach ($data as $item) {
// Get the current stadium in the list
$criteria = array( 'stadiumID' => $item['StadiumID'] );
$currentStadium = $repo->FindOneBy( $criteria );
if ( ! $currentStadium) {
$currentStadium = $this->createNewStadium; //no stadium with the StadiumID use the new stadium entity
}
$currentStadium->setStadiumID( $item['StadiumID'] );
$currentStadium->setName( $item['Name'] );
$currentStadium->setCity( $item['City'] );
$currentStadium->setState( $item['State'] );
$currentStadium->setCountry( $item['Country'] );
$currentStadium->setCapacity( $item['Capacity'] );
$currentStadium->setPlayingSurface( $item['PlayingSurface'] );
$this->em->persist( $currentStadium );
}
$this->em->flush();
}
// Adding this new method gives you the ability to mock this dependency when testing
private function createNewStadium()
{
return new Stadium();
}
}
What you basically need is Unit Testing the service using what is called "Test doubles".
This means you should mock the dependencies your service has, this way you are able to test only the service in isolation without really relying on the deps but only on a mocked version of them, with hardcoded values or behaviour.
A true example based on your real implementation isn't possible since you have tight coupled deps as $currentStadium = new Stadium();. You should pass deps like these in a constructor or via getter/setter in order to be able to mock it when Unit testing.
Once done it a very indicative example would be:
// class StadiumParser revisited and simplified
class StadiumParser
{
private $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function parseData()
{
$stadiumData = $this->client->Stadiums();
// do something with the repo
$log = array();
foreach ($stadiumData as $stadium) {
$logData = [
'action' => 'Added Stadium',
'itemID' => $stadium['StadiumID'],
'itemName' => $stadium['Name']
];
$log[] = $logData;
}
// do something else with Doctrine
return $log;
}
}
and the test
// StadiumParser Unit Test
class StadiumParserTest extends PHPUnit_Framework_TestCase
{
public function testItParseDataAndReturnTheLog()
{
$client = $this->getMock('FantasyDataAPI\Client');
// since you class is returning a log array, we mock it here
$expectedLog = array(
array(
'action' => 'Added Stadium',
'itemID' => $stadium['StadiumID'],
'itemName' => $stadium['Name']
)
);
// this is the mocked or test double part.
// We only need this method return something without really calling it
// So we mock it and we hardcode the expected return value
$stadiumData = array(
array(
"StadiumID" => 1,
"Name" => "aStadiumName"
)
);
$client->expects($this->once())
->method('Stadiums')
->will($this->returnValue($stadiumData));
$stadiumParser = new StadiumParser($client);
$this->assertEquals($expectedLog, $stadiumParser->parseData());
}
}
I voluntarily omitted the EntityManager part because I guess you should have a look at the Symfony Doc relative to how to unit test code interacting with the database
-----EDIT2-----
Yes he was right, you shouldn't. One possible way that comes in mind is to extract the creation of the entity in a protected/private method. Something like:
// class StadiumParser
public Function parseData()
{
...
foreach ($stadiumData as $stadium) {
...
if ( ! $currentStadium) {
$currentStadium = $this->createNewStadium();
...
}
// Adding this new method gives you the ability to mock this dependency when testing
private function createNewStadium()
{
return new Stadium();
}
-----EDIT3-----
I want to suggest you another approach. This should be, probably, a better choice should the Stadium entity needed in different services or different part of the same. What I'm proposing is called Builder pattern but a Factory could also be an option here. Browse a bit for their differences.
As you can see this extract some code from the method, distribute better the responsibility between the classes and leaves all cleaner and easier to read for you and your teammates. And you already know how to mock it when testing.
class StadiumParser
{
private $stadiumBuilder;
...
public function __construct( StadiumBuilder $builder, ...) {
$this->stadiumBuilder = $stadiumBuilder;
...
}
public Function parseData()
{
...
foreach ($stadiumData as $stadium) {
...
$currentStadium = $repo->FindOneBy( $criteria );
if ( ! $currentStadium) {
$currentStadium = $this->stadiumBuilder->build($currentStadium, $stadium);
}
$this->em->persist($currentStadium);
...
somewhere you have this new Builder that return a Stadium instance. this way your StadiumParser service is not coupled anymore with the entity, but the StadiumBuilder is it. The logic is something like:
// StadiumBuilder class
namespace ???
use FantasyPro\DataBundle\Entity\Stadium;
class StadiumBuilder
{
// depending on the needs but this should also has a different name
// like buildBasic or buildFull or buildBlaBlaBla or buildTest
public function build($currentStadium = null, $stadium)
{
if (!$currentStadium) {
$currentStadium = new Stadium();
}
$currentStadium->setStadiumID( $stadium['StadiumID'] );
$currentStadium->setName( $stadium['Name'] );
$currentStadium->setCity( $stadium['City'] );
$currentStadium->setState( $stadium['State'] );
$currentStadium->setCountry( $stadium['Country'] );
$currentStadium->setCapacity( $stadium['Capacity'] );
$currentStadium->setPlayingSurface( $stadium['PlayingSurface'] );
return $currentStadium;
}
}
Related
I'm actually testing my api code written with:
symfony 4
api-platform
FOS User
JWT
I use codeption for my tests and everything is ok so far.
For several entities, I fire onFlush doctrine callback and it's working just fine when authenticated from my front application in react.
At this point I get my authenticated user in the callback via an injected security component.
However when doing the same things via codeception, even if onFlush is fired, I'm not able to retrieve my user neither the token via the security injection.
I tried to inject the token instead, also the entire service container, none has worked.
This is my OnFlush class:
{
/**
* #var Security
*/
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function onFlush(OnFlushEventArgs $args): void
{
$user = $this->security->getUser();
...
And here how I set my authorization header in codeception test:
$I->haveHttpHeader('Authorization', 'Bearer ' . $token);
$I->sendPUT(
'/entity/uuid.json',
[
'attribute' => $value
]
);
I would like to get the user having the specified token whe executing the test in the callback.
PS: Before executing the PUT test, I did the same thing with GET and just got the related entities, when I remove Authorization header I do get all users entities. It seems that it's not working only in callback.
Thanks
After a lot research, it's obviously a codeception problem.
I ended up making this particular test with phpunit as codeception couldn't load the service container in doctrine events.
If you try to edit your services.yaml file and to execute your tests, it works on first time as the service container is re-built (re-cached).
But once cached, it will always return an empty container (without tokenstrorage, security, ...).
Creating a helper method to provide the user wouldn't work neither, I'll leave the code here in case of need:
/**
* Create user or administrator and set auth cookie to client
*
* #param string $user
* #param string $password
* #param bool $admin
*/
public function setAuth(string $user, string $password, bool $admin = false): void
{
/** #var Symfony $symfony */
try {
$symfony = $this->getModule('Symfony');
} catch (ModuleException $e) {
$this->fail('Unable to get module \'Symfony\'');
}
/** #var Doctrine2 $doctrine */
try {
$doctrine = $this->getModule('Doctrine2');
} catch (ModuleException $e) {
$this->fail('Unable to get module \'Doctrine2\'');
}
$user = $doctrine->grabEntityFromRepository(User::class, [
'username' => $user
]);
$token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
$symfony->grabService('security.token_storage')->setToken($token);
/** #var Session $session */
$session = $symfony->grabService('session');
$session->set('_security_main', serialize($token));
$session->save();
$cookie = new Cookie($session->getName(), $session->getId());
$symfony->client->getCookieJar()->set($cookie);
}
Creating a phpunit test with below code would do the job just fine:
/**
* #param string $method
* #param string $url
* #param array $content
* #param bool $authorization
*/
protected static function performRequest (string $method, string $url, array $content = [], $authorization = false): void
{
$headers = [
'CONTENT_TYPE' => 'application/json',
];
if ($authorization)
{
$headers = array_merge($headers, [
'HTTP_AUTHORIZATION' => 'Bearer ' . self::$token
]);
}
self::$client->request(
$method,
'/api/' . $url,
[],
[],
$headers,
json_encode($content)
);
}
I got exactly the same problem. Here is my solution:
<?php
use Codeception\Stub;
use Codeception\Module\Doctrine2;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\Security;
class YourEventSubscriberCest
{
/**
* #var Security
*/
protected Security $security;
/**
* #param FunctionalTester $I
*/
public function _before(FunctionalTester $I, Doctrine2 $doctrine2)
{
$user = new User();
$security = Stub::makeEmpty(Security::class, [
'getUser' => $user,
]);
$backup = $this->replaceSecurity($doctrine2, $security);
if ($backup instanceof Security) {
$this->security = $backup;
}
}
public function _after(FunctionalTester $I, Doctrine2 $doctrine2)
{
$this->replaceSecurity($doctrine2, $this->security);
}
protected function replaceSecurity(Doctrine2 $doctrine2, object $newSecurity): ?object
{
$listeners = $doctrine2->_getEntityManager()->getEventManager()->getListeners('onFlush');
foreach ($listeners as $listener) {
if ($listener instanceof YourEventSubscriber) {
$reflection = new \ReflectionObject($listener);
$property = $reflection->getProperty('security');
$property->setAccessible(true);
$oldSecurity = $property->getValue($listener);
$property->setValue($listener, $newSecurity);
return $oldSecurity;
}
}
}
}
First I will explain why and how the solution works and then the problems I have encountered. If you think there is a better way to do what I do, I'd love to hear it. I would also like to know why doctrine behaves in this way.
It turns out that my aplication needs to connect to a different database according to the client. I have a table, in a fixed database, containing the connection information that is used in some request.
I have had success with the following code:
class DynamicEntityManager {
protected $em;
private $request;
private $client_id;
public function __construct(RequestStack $request, EntityManagerInterface $em){
$this->em = $em;
$this->request = $request;
}
public function getEntityManager(ClientConn $client = null) {
$request = $this->request->getCurrentRequest();
if($client == NULL){
$domain = $request->attributes->get('domain');
if($domain == "" || $domain == NULL){
throw new \Exception("Error de conexion", 1);
}
$client = $this->em->getRepository(ClientConn::class)->findOneBy(array(
"subdomain" => $domain
));
if($client == NULL){
throw new \Exception("Error de conexion", 1);
}
}
$connectionDB = $client->getConnection();
$dbdriver = 'oci8';
$conexionSplit = explode(':',$connectionDB);
$dbhost = $conexionSplit[0];
$dbport = $conexionSplit[1];
$dbname = $conexionSplit[2];
$dbuser = $client->getUsuarioBd();
$dbpass = $client->getClaveBd();
$service = false;
$this->client_id = $client->getId();
if(strpos($dbname,'SN=') !== false){
$parts = explode('=',$dbname);
$dbname = $parts[1];
$service = true;
}
$request->attributes->set('client_id',$client->getId());
$conn = array(
'driver' => $dbdriver,
'host' => $dbhost,
'port' => $dbport,
'dbname' => $dbname,
'user' => $dbuser,
'password' => $dbpass,
'service' => $service,
'charset' => 'UTF8',
'schema' => null
);
return EntityManager::create($conn, $this->em->getConfiguration());
}
}
As you can see I return EntityManager::create($conn, $this->em->getConfiguration ()) with the new connection. The way I use it is the next:
/**
* #Route("/api/client/{id}/conf/{confID}", name="conf.show")
* #Method({"GET"})
*/
public function show(ClientConn $client, Request $request, DynamicEntityManager $dem ,$confId){
try {
$em = $dem->getEntityManager($client);
$entity = $em->getRepository(Configuration::class)->find($confId);
return new JsonResponse($entity, 200);
}
catch(\Exception $ex) {
return new JsonResponse([
"excepcion" => $ex->getMessage()
], $ex->getCode());
}
}
It works as expected or so I believed until I saw that when the entity has a custom repository it is unable to use the dynamic connection and therefore the previous route will return a table not found exception.
#ORM\Entity() <-- Works like a charm
#ORM\Entity(repositoryClass="App\Repository\ConfigurationRepository")<-- Table not found.
It works in the repository if I create the connection again, although I do not like the solution. So, what do I want? I would like to be able to use the basic methods like find (), findBy () and others without having to rewrite them every time I use a custom repository.
class ConfigurationRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry, DynamicEntityManager $dem)
{
parent::__construct($registry, Configuration::class);
$this->dem= $dem;
}
public function uglyFind($client, $confID)
{
$query = $this->dem->getEntityManager($client)->createQueryBuilder('conf')
->select("conf")
->from(ConfPedidosLentes::class,'conf')
->where('conf.id = :value')->setParameter('value', $confID)
->getQuery();
return $query->getOneOrNullResult();
}
I will really appreciate any contribution and thought in this matter.
Instead of:
class ConfigurationRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry, DynamicEntityManager $dem)
{
parent::__construct($registry, Configuration::class);
$this->dem= $dem;
}
...
try extending EntityRepository (without using a constructor) and use find as you did in your controller:
use Doctrine\ORM\EntityRepository;
class ConfigurationRepository extends EntityRepository
{
}
ServiceEntityRepository is an optional EntityRepository base class with a simplified constructor for autowiring, that explicitly sets the entity manager to the EntityRepository base class. Since you have not configured your doctrine managers to handle these connections properly (it's not even possible actually with so many connections), ServiceEntityRepository will pass a wrong EntityManager instance to the EntityRepository subclass, that's why you should not extend ServiceEntityRepository but EntityRepository.
I am receiving the following error
[2018-12-18 12:12:46] local.ERROR: Credentials are required to create a Client {"exception":"[object] (Twilio\Exceptions\ConfigurationException(code: 0): Credentials are required to create a Client at C:\wamp64\www\_javid\javid\vendor\twilio\sdk\Twilio\Rest\Client.php:157)
I will include the code below and the source i used to create it. I would like to add, this was all working correctly the other evening.
Today, i merely added a new function to handle the saving of messages to the database. Then i started receiving the above error. Naturally i reverted my changes but still the same error.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\User;
use Illuminate\Support\Facades\Auth;
use JWTAuth;
use App\Item;
use Log;
use Twilio\Rest\Client;
class MessagingController extends Controller
{
protected $client;
public function __construct(Client $client){
$this->client = $client;
}
/**
* Show the form for creating a notification.
*
* #return \Illuminate\Http\Response
*/
public function create()
{
return view('notifications.create');
}
public function sendMessage(request $request){
$details = $request->only('membershipNumber', 'countryCode', 'message');
$user = User::where('membership_number', $details['membershipNumber'])->with('mobile_number')->first();
if(count($user)>0){
$this->messageSaveToDatabase($details, $user);
$this->messageSendToMobile($details, $user);
$this->messageSendToEmail($details, $user);
return response([
'status' => 'success',
'msg' => __('messages.success'),
'response' => $details
], 200);
} else {
return response([
'status' => 'error',
'msg' => __('messages.error')
], 200);
}
}
protected function messageSaveToDatabase($details, $user){
}
protected function messageSendToMobile($details, $user, $imageUrl = null){
$lineBreak = "\n\n";
$phoneNumber = $user->mobile_number->country_code.decrypt($user->mobile_number->number);
$message = "Hi member #".$details['membershipNumber'].$lineBreak.
$details['message'];
$twilioPhoneNumber = config('services.twilio')['phoneNumber'];
$messageParams = array(
'from' => $twilioPhoneNumber,
'body' => $message
);
if ($imageUrl) {
$messageParams['mediaUrl'] = $imageUrl;
}
$this->client->messages->create(
$phoneNumber,
$messageParams
);
}
protected function messageSendToEmail($details, $user){
}
}
I have checked the TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN, these are both correct.
The code was taken from the following guide, i stripped out the subscriber part. Guide from Twilio
one more thing, I found the following Here which suggests i need to do something like this $client = new Client($keySid, $keySecret, $accountSid); but the guide above, does not do this, plus it all worked like this also.
Any help or suggestions would be great, i'm running out of hair to pull out :(
After a little more googling and some re-working, I found a working solution
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\User;
use Illuminate\Support\Facades\Auth;
use Twilio\Rest\Client;
class MessagingController extends Controller
{
protected function messageSendToMobile($details, $message, $user, $imageUrl = null){
$accountSid = env('TWILIO_ACCOUNT_SID');
$authToken = env('TWILIO_AUTH_TOKEN');
$twilioNumber = env('TWILIO_PHONE_NUMBER');
$lineBreak = "\n\n";
$to = $user->mobile_number->country_code.decrypt($user->mobile_number->number);
$client = new Client($accountSid, $authToken);
try {
$client->messages->create(
$to,
[
"body" => $message,
"from" => $twilioNumber
]
);
Log::info('Message sent to ' . $twilioNumber);
} catch (TwilioException $e) {
Log::error(
'Could not send SMS notification.' .
' Twilio replied with: ' . $e
);
}
}
}
Updated question Title
Sorry if the title of this question is a little off, I'm really not sure how to phrase it.
I'm trying to create a service that will pull data from a json feed and persist/flush it to my database via doctrine.
After much searching/reading/trial and error I've managed to get my service registered:
services:
fantasyapi:
class: FantasyDataAPI\Client
arguments: ["%fantasyapi.key%"]
data_manager:
class: FantasyPro\DataBundle\DataManager\StadiumParser
arguments: ['#doctrine.orm.entity_manager', '#fantasyapi', ::DataBundle.Entity.Stadium]
I'm using php storm with the symfony2 plugin.
If i use:
$repo = $this->em->getRepository('DataBundle:Stadium');
PHP storm reports it as an undefined function.
However if i use
$repo = $this->em->em->getRepository('DataBundle:Stadium');
php storm does not report it as undefined.
I think i'm doing something wrong as using ->em->em->getRepository does not feel right.
Am i instantiating the service correctly and if so why do i have to duplicate the reference to the function?
heres the full code of my service:
<?php
namespace FantasyPro\DataBundle\DataManager;
use Doctrine\ORM\EntityManager;
use FantasyDataAPI\Client;
use FantasyPro\DataBundle\Entity\Stadium;
class StadiumParser {
private $em;
private $client;
private $stadium;
/**
* #param EntityManager $em
* #param Client $client
* #param Stadium $stadium
*/
public function __constuct(EntityManager $em, Client $client, Stadium $stadium ) {
$this->em = $em;
$this->client = $client;
$this->stadium = $stadium;
}
/**
* #return array
*/
public Function parseData(){
$stadiumData = $this->client->client->Stadiums();
//get the entity manager
$repo = $this->em->em->getRepository('DataBundle:Stadium');
$log = array();
foreach ($stadiumData as $stadium) {
// Get the current stadium in the list
$criteria = array( 'stadiumID' => $stadium['StadiumID'] );
$currentStadium = $repo-->FindOneBy( $criteria );
if ( ! $currentStadium) {
$currentStadium = new Stadium(); //no stadium with the StadiumID exists so create a new stadium
$logData = [
'action' => 'Added Stadium',
'itemID' => $stadium['StadiumID'],
'itemName' => $stadium['Name']
];
$log[] = $logData;
} else {
$logData = [
'action' => 'Updated '.$logTitle,
'itemID' => $stadium['PlayerID'],
'itemName' => $stadium['Name']
];
$log[] = $logData;
}
$currentStadium->setStadiumID( $stadium['StadiumID'] );
$currentStadium->setName( $stadium['Name'] );
$currentStadium->setCity( $stadium['City'] );
$currentStadium->setState( $stadium['State'] );
$currentStadium->setCountry( $stadium['Country'] );
$currentStadium->setCapacity( $stadium['Capacity'] );
$currentStadium->setPlayingSurface( $stadium['PlayingSurface'] );
$this->em->em->persist( $currentStadium );
}
$this->em->em->flush();
return $log;
}
}
You can document the type of each member of your class:
<?php
namespace FantasyPro\DataBundle\DataManager;
use Doctrine\ORM\EntityManager;
use FantasyDataAPI\Client;
use FantasyPro\DataBundle\Entity\Stadium;
class StadiumParser
{
/** #var EntityManager em */
private $em;
/** #var Client client */
private $client;
/** #var Stadium stadium */
private $stadium;
/* ... */
For some reason PHPStorm didn't recognized the classes when I only used the class name so I had to put the full namespace, e.g.:
/* ... */
/** #var \Doctrine\ORM\EntityManager em */
private $em;
/* ... */
Accessing my route /message/new i'm going to show a form for sending a new message to one or more customers. Form model has (among others) a collection of Customer entities:
class MyFormModel
{
/**
* #var ArrayCollection
*/
public $customers;
}
I'd like to implement automatic customers selection using customers GET parameters, like this:
message/new?customers=2,55,543
This is working now by simply splitting on , and do a query for getting customers:
public function newAction(Request $request)
{
$formModel = new MyFormModel();
// GET "customers" parameter
$customersIds = explode($request->get('customers'), ',');
// If something was found in "customers" parameter then get entities
if(!empty($customersIds)) :
$repo = $this->getDoctrine()->getRepository('AcmeHelloBundle:Customer');
$found = $repo->findAllByIdsArray($customersIds);
// Assign found Customer entities
$formModel->customers = $found;
endif;
// Go on showing the form
}
How can i do the same using Symfony 2 converters? Like:
public function newAction(Request $request, $selectedCustomers)
{
}
Answer to my self: there is not such thing to make you life easy. I've coded a quick and dirty (and possibly buggy) solution i'd like to share, waiting for a best one.
EDIT WARNING: this is not going to work with two parameter converters with the same class.
Url example
/mesages/new?customers=2543,3321,445
Annotations:
/**
* #Route("/new")
* #Method("GET|POST")
* #ParamConverter("customers",
* class="Doctrine\Common\Collections\ArrayCollection", options={
* "finder" = "getFindAllWithMobileByUserQueryBuilder",
* "entity" = "Acme\HelloBundle\Entity\Customer",
* "field" = "id",
* "delimiter" = ",",
* }
* )
*/
public function newAction(Request $request, ArrayCollection $customers = null)
{
}
Option delimiter is used to split GET parameter while id is used for adding a WHERE id IN... clause. There are both optional.
Option class is only used as a "signature" to tell that converter should support it. entity has to be a FQCN of a Doctrine entity while finder is a repository method to be invoked and should return a query builder (default one provided).
Converter
class ArrayCollectionConverter implements ParamConverterInterface
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
function apply(Request $request, ConfigurationInterface $configuration)
{
$name = $configuration->getName();
$options = $this->getOptions($configuration);
// Se request attribute to an empty collection (as default)
$request->attributes->set($name, new ArrayCollection());
// If request parameter is missing or empty then return
if(is_null($val = $request->get($name)) || strlen(trim($val)) === 0)
return;
// If splitted values is an empty array then return
if(!($items = preg_split('/\s*'.$options['delimiter'].'\s*/', $val,
0, PREG_SPLIT_NO_EMPTY))) return;
// Get the repository and logged user
$repo = $this->getEntityManager()->getRepository($options['entity']);
$user = $this->getSecurityContext->getToken()->getUser();
if(!$finder = $options['finder']) :
// Create a new default query builder with WHERE user_id clause
$builder = $repo->createQueryBuilder('e');
$builder->andWhere($builder->expr()->eq("e.user", $user->getId()));
else :
// Call finder method on repository
$builder = $repo->$finder($user);
endif;
// Edit the builder and add WHERE IN $items clause
$alias = $builder->getRootAlias() . "." . $options['field'];
$wherein = $builder->expr()->in($alias, $items);
$result = $builder->andwhere($wherein)->getQuery()->getResult();
// Set request attribute and we're done
$request->attributes->set($name, new ArrayCollection($result));
}
public function supports(ConfigurationInterface $configuration)
{
$class = $configuration->getClass();
// Check if class is ArrayCollection from Doctrine
if('Doctrine\Common\Collections\ArrayCollection' !== $class)
return false;
$options = $this->getOptions($configuration);
$manager = $this->getEntityManager();
// Check if $options['entity'] is actually a Dcontrine one
try
{
$manager->getClassMetadata($options['entity']);
return true;
}
catch(\Doctrine\ORM\Mapping\MappingException $e)
{
return false;
}
}
protected function getOptions(ConfigurationInterface $configuration)
{
return array_replace(
array(
'entity' => null,
'finder' => null,
'field' => 'id',
'delimiter' => ','
),
$configuration->getOptions()
);
}
/**
* #return \Doctrine\ORM\EntityManager
*/
protected function getEntityManager()
{
return $this->container->get('doctrine.orm.default_entity_manager');
}
/**
* #return \Symfony\Component\Security\Core\SecurityContext
*/
protected function getSecurityContext()
{
return $this->container->get('security.context');
}
}
Service definition
arraycollection_converter:
class: Acme\HelloBundle\Request\ArrayCollectionConverter
arguments: ['#service_container']
tags:
- { name: request.param_converter}
It's late, but according to latest documentation about #ParamConverter, you can achieve it follow way:
* #ParamConverter("users", class="AcmeBlogBundle:User", options={
* "repository_method" = "findUsersByIds"
* })
you just need make sure that repository method can handle comma (,) separated values