I have a problem asserting updates made by a form are really written in the database.
I explain, I first do a create test (testCreateLactationForm), very similar to the edit test. In that firs test y check the record is finally inserted and all works fine:
$finalRecords = $qb->where($qb->expr()->like('p.comments', ':comments'))
->setParameter('comments', '%'.$payload["form[comments]"].'%')
->getQuery()
->getResult();
$this->assertTrue(count($finalRecords) == (count($initialRecords) + 1));
After that, I made a dependant test where I locate las record, navigate to edit form, and update the modality and comments fields. In last line, when asserting Modality is equal to ONE_HOUR_REDUCTION, entity modality field remains with original creation value, so it fails, while in database the value was succesfully updated.
class HhrrProceduresControllerTest extends WebTestCase
{
/** #var \Doctrine\ORM\EntityManager */
private $entityManager;
private $client;
public function setUp()
{
parent::setUp();
$this->client = self::createClient();
$kernel = self::bootKernel();
$this->entityManager = $kernel
->getContainer()
->get('doctrine')
->getManager();
$this->hrmLogger = $kernel
->getContainer()
->get('monolog.logger.hrm');
}
[...]
/**
* #depends testCreateLactationForm
*/
public function testEditLactationProcedure()
{
$payload = [
"hhrr_procedure_lactation[modality]" => HhrrProcedureLactationModality::ONE_HOUR_REDUCTION,
"hhrr_procedure_lactation[iniDate]" => "07/02/2022",
"hhrr_procedure_lactation[endDate]" => "13/02/2022",
"hhrr_procedure_lactation[hhrrProcedure][comments]" => "XXX--TEST--XXX | UPDATED",
];
$qb = $this->entityManager->getRepository(HhrrProcedure::class)->createQueryBuilder('p');
$initialRecord = $qb->orderBy('p.id', 'DESC')
->setMaxResults(1)
->getQuery()
->getResult();
$this->client->followRedirects(true);
$this->client->request('GET', '/rrhh/tramites/'.$initialRecord[0]->getId().'/editar-tramite/', [], [], ['HTTP_X-AUTH-USERNAME' => $this->logged_in_user]);
$crawler = $this->client->submitForm('Siguiente', $payload);
$this->writeOutput(__FUNCTION__, $this->client->getResponse()->getContent());
// verificar retorno correcto de la página
$this->assertSame(200, $this->client->getResponse()->getStatusCode());
/** #var HhrrProcedure $updatedRecord */
$updatedRecord = $this->entityManager->getRepository(HhrrProcedure::class)->find($initialRecord[0]->getId());
/** FAILS --> **/ $this->assertEquals($updatedRecord->getHhrrProcedureLactation()->getModality(),HhrrProcedureLactationModality::ONE_HOUR_REDUCTION);
}
In addition to that, if I try to write aditional dependant test, assertion is ok...
/**
* #depends testEditLactationProcedure
*/
public function testEditLactationProcedureAssertion()
{
$qb = $this->entityManager->getRepository(HhrrProcedure::class)->createQueryBuilder('p');
$initialRecord = $qb->orderBy('p.id', 'DESC')
->setMaxResults(1)
->getQuery()
->getResult();
$this->assertEquals($initialRecord[0]->getHhrrProcedureLactation()->getModality(),HhrrProcedureLactationModality::ONE_HOUR_REDUCTION);
}
Some help? Thanks!!
Doctrine entities are persisted in an internal cache (which is not the case when you use findBy methods, that load a new documents). You need to use
EntityManagerInterface's refresh($entityObject) or clear() in order to get fresh data from the database.
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;
}
}
}
}
Orders // orders
Comments // comments for every order
I would like to find latest comment written in this order.
My
Controller:
$orders = $this->getDoctrine()->getRepository(Orders::class)->findAll();
foreach($orders as $order) {
$temp = array(
$order->getId(),
$order->getComments()->findLatest( $order->getId() )
Entity (Comments):
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Orders", inversedBy="comments")
*/
private $orders;
Entity(Order):
/**
* #return Collection|Comment[]
*/
public function getComments(): Collection
{
return $this->comments;
}
Comment Repository:
public function findLatest($value)
{
return $this->createQueryBuilder('c')
->andWhere('c.orders = :val')
->setParameter('val', $value)
->orderBy('c.id', 'DESC')
->setMaxResults(1)
->getQuery()
->getResult()
;
}
But looks like it not working in this way :(
Error:
Attempted to call an undefined method
named "findLatest" of class "Doctrine\ORM\PersistentCollection".
you are trying to call a repository function from another entity
try to change this line :
$order->getComments()->findLatest( $order->getId()
with:
$this->getDoctrine()->getRepository(Comments::class)->findLatest($order->getId);
a better soulution will be that you work with $orders->getComments() array to avoid requesting data from the database inside a loop
You can do this using the class Doctrine\Common\Collections\Criteria.
Entity(Order):
use Doctrine\Common\Collections\Criteria;
...
/**
* Returns the latest comment or false if no comments found under that criteria
*/
public function findLatestComment()
{
$criteria = Criteria::create()
->orderBy(array("id" => Criteria::DESC))
;
return $this->getComments()->matching($criteria)->first();
}
And then you can simply use it like this:
$order->findLatestComment();
Well, back again, i'll try to simplify my question as much as i can.
First of all, i have 2 Entities
Post
PostRating
I've created unidirectional ManyToMany relation between them, because I only need ratings to be added to each Post, if I try to map Post to PostRating too, I get Circular Reference error.
Post Entity, it creates 3rd table post_has_rating, no mapping inside PostRating Entity, It workes like expected, rating collection is added to each post, but if i want to find one rating, and edit it if needed, then it comes to be bigger headache than expected.
/**
* Post have many PostRating
* #ORM\ManyToMany(targetEntity="PostRating")
* #ORM\JoinTable(name="post_has_rating",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="postrating_id", referencedColumnName="id", unique=true)}
* )
*/
protected $ratings;
PostController thumbAction, simple word "ratingAction"
/**
* Search related videos from youtube
* #Route("/post/thumb", name="post_thumb")
* #param Request $request
* #return string
*/
public function thumbAction (Request $request) {
$content = json_decode($request->getContent());
$serializer = $this->get('serializer');
$em = $this->getDoctrine()->getManager();
$postRatingRepo = $this->getDoctrine()->getRepository(PostRating::class);
$postRepo = $this->getDoctrine()->getRepository(Post::class);
$me = $this->getUser()->getId();
/** #var PostRating $rating */
$rating = $postRatingRepo->findOneBy(['userId' => $me]);
/** #var Post $post */
$post = $postRepo->find($content->id);
if ($post->getRatings()->contains($rating)) {
$post->removeRating($rating);
$em->remove($rating);
}
$rating = new PostRating();
$rating->setUserId($me);
switch ($content->action) {
//NVM about those hardcoded words, they are about to be removed
case 'up':
$rating->setRating(1);
break;
case 'down':
$rating->setRating(0);
break;
}
$post->addRating($rating);
$em->persist($rating);
$em->persist($post);
$em->flush();
return new JsonResponse( $serializer->normalize( ['success' => 'Post thumbs up created'] ) );
}
Problems: $rating = $postRatingRepo->findOneBy(['userId' => $me]); this row needs to have postId too for $post->getRatings()->contains($rating), right now im getting all the raitings, that I have ever created, but it Throws error if i add it, Unknown column
Should i create custom repository, so i can create something like "findRating" with DQL?
OR
Can i make Post and PostRating Entities mapped to each other more simple way, i don't really want many-to-many relation, because I don't see point of using it
Considering you want to keep OneToMany unidirectional here is my suggestion
create a custom repository for your Post Entity
namespace AppBundle\Repository;
use Doctrine\ORM\EntityRepository;
class PostRepository extends EntityRepository
{
public function findOneRatingByUser($post, $user)
{
$query = $this->createQueryBuilder('p')
->select('r')
->innerJoin('p.ratings', 'r')
->where('p.id = :post')
->andWhere('r.user = :user')
->setParameter('post', $post)
->setParameter('user', $user)
->getQuery()
;
return $query->getOneOrNullResult();
}
}
Then in your controller:
public function thumbAction (Request $request)
{
$content = json_decode($request->getContent());
$serializer = $this->get('serializer');
$em = $this->getDoctrine()->getManager();
$postRepo = $this->getDoctrine()->getRepository(Post::class);
$me = $this->getUser()->getId();
/** #var Post $post */
$post = $postRepo->find($content->id);
$rating = $postRepo->findOneRatingByUser($post->getId(), $me);
if (null === $rating) {
$rating = new PostRating();
$rating->setUserId($me);
}
switch ($content->action) {
//NVM about those hardcoded words, they are about to be removed
case 'up':
$rating->setRating(1);
break;
case 'down':
$rating->setRating(0);
break;
}
$post->addRating($rating);
$em->persist($rating);
$em->persist($post);
$em->flush();
return new JsonResponse( $serializer->normalize( ['success' => 'Post thumbs up created'] ) );
}
If you want your custom repository to work dont forget to declare it in your entity
/**
* #ORM\Entity(repositoryClass="AppBundle\Repository\PostRepository")
*/
class Post
I've searched a lot about this, and seriously asking is my last resource, doctrine is kicking me hard.
I have an entity named "Contract" and another "Request", a Contract may have several Requests, when adding a new Request I search for an existent contract of that client and associate it if already exists or create it if not.
In RequestRepository.php:
public function findOrCreate($phone)
{
$em = $this->getEntityManager();
$contract = $this->findOneBy(array('phone' => $phone));
if($contract === null)
{
$contract = new Contract();
$contract->setPhone($phone)
->setDesDate(new \DateTime());
# save only if new
$em->persist($contract);
}
return $contract;
}
The thing is, when the contract is new it works ok, but when is "reused" from db I can't modify its attributes. I checked the OneToMany and ManyToOne already.
In Contract.php:
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\OneToMany(targetEntity="Request", mappedBy="contract")
*/
private $id;
In Request.php:
/**
* #var string
*
* #ORM\JoinColumn(nullable=false)
* #ORM\ManyToOne(targetEntity="Cid\FrontBundle\Entity\Contract", inversedBy="id", cascade={"persist"})
*/
protected $contract;
I also have a method which modifies an attribute within Contract.php:
public function addTime($months)
{
$days = $months * 30;
$this->des_date->add(new \DateInterval("P".$days."D"));
return $this;
}
I create the request and "findOrCreate" a contract, but if the later is not "fresh" the addTime does not save to db.
What am I doing wrong?
Edit: The controller is a common CRUD with minor modifications.
Don't worry about "request" name clash, the actual code is in spanish, Request = Solicitud
public function createAction(Request $req)
{
$entity = new Request();
$form = $this->createForm(new RequestType(), $entity);
$form->bind($req);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$entity->setUser($this->getUser());
$data = $request->request->get('cid_frontbundle_requesttype');
$phone = $data['phone_number'];
$reqRep = $em->getRepository('FrontBundle:Request');
$entity = $reqRep->newRequest($entity, $phone);
return $this->redirect($this->generateUrl('request_show', array('id' => $entity->getId())));
}
return $this->render('FrontBundle:Request:new.html.twig', array(
'entity' => $entity,
'form' => $form->createView(),
));
}
The newRequest:
public function newRequest($request, $phone)
{
$em = $this->getEntityManager();
$contractRep = $em->getRepository('FrontBundle:Contract');
$contract = $contractRep->findOrCreate($phone);
$contract->addTime(123); # this is the problem, I use var_dump and this method works, but doesn't persists
$em->persist($request);
$em->flush();
return $request;
}
Eureka!! The issue was that doctrine seems to check the objects by reference, and all I did with the contract was adding a DateInterval to a DateTime property, so the object was the same for doctrine's matter and there was no saving. This is the code that made it.
public function addTime($months)
{
$days = $months * 30; # I know DateInterval has months but this is company policy ;)
$other = new \DateTime($this->des_date->format('Y-m-d')); # creating a brand new DateTime did the trick
$other->add(new \DateInterval("P".$days."D"));
$this->des_date = $other;
return $this;
}
Thanks for everything #cheesemacfly.
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