First of I all, I created the whole example below specifically for this question because the actual example is very big so if it looks stupid then assume that it is not for now!
I'm trying to come up with a solution so that I can call a correct private method (bankA() or bankB()) in controller if the validation successfully passes. As you can see in the custom validation constraint, I only check the $bank->code property however the condition is not actually that simple (there is repository checks so on) - (as I said above, it is trimmed down version). So, could please someone tell me, how will I know that which private method I should call in controller after successful validation? I'm happy to create dedicated validators if necessary so open for suggestions and examples.
Note: I looked into symfony group validation documentation but didn't really get the picture how I could apply to my scenario.
EXAMPLE REQUEST
{ "id": 66, "code": "A" }
{ "id": 34, "code": "B" }
CONTROLLER
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* #Route("bank", service="application_frontend.controller.bank")
*/
class BankController extends Controller
{
private $validator;
public function __construct(
ValidatorInterface $validator
) {
$this->validator = $validator;
}
/**
* #param Request $request
*
* #Route("")
* #Method({"POST"})
*
* #throws Exception
*/
public function indexAction(Request $request)
{
$content = $request->getContent();
$content = json_decode($content, true);
$bank = new Bank();
$bank->id = $content['id'];
$bank->code = $content['code'];
$errors = $this->validator->validate($bank);
if (count($errors)) {
throw new Exception($errors[0]->getMessage());
}
// OK, validation has passed so which one do I call now ?!?!
$this->bankA($bank);
$this->bankB($bank);
}
private function bankA(Bank $bank)
{
// Do something nice with Bank
}
private function bankB(Bank $bank)
{
// Do something bad with Bank
}
}
BANK MODEL
use Application\FrontendBundle\Validator\Constraint as BankAssert;
/**
* #BankAssert\Bank
*/
class Bank
{
/**
* #var int
*/
public $id;
/**
* #var string
*/
public $code;
}
CUSTOM VALIDATOR
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class Bank extends Constraint
{
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
public function validatedBy()
{
return get_class($this).'Validator';
}
}
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class BankValidator extends ConstraintValidator
{
public function validate($bank, Constraint $constraint)
{
if ($bank->code == 'A') {
return;
}
if ($bank->code == 'B') {
return;
}
$this->context->buildViolation('Bank error')->addViolation();
}
}
Depending on how many codes there are you could either do...
if ('A' === $bank->getCode()) {
$this->bankA($bank);
} else {
$this->bankB($bank);
}
Or..
$method = 'bank'.$bank->getCode();
if (!method_exists($this, $method)) {
throw new \Exception('Method "'.$method.'" does not exist');
}
$this->$method();
All of that being said, it would be advisable to move all of this work into a dedicated service rather than in your controller. Then in your controller use something like...
$this->container->get('do_something_to_bank.service')->processAction($bank);
Related
I have a Task entity, with two mandatory, non-nullable, fields:
title
dueDatetime
and Form to create task. The form is called by external scripts through POST with application/x-www-form-urlencoded (so no json or anything fancy), so I use standard symfony to handle this.
Problem is I don't control the scripts, and if the script forgot one of the argument, symfony4 will directly throw an exception at the handleRequest step, before I have the time to check if the form is valid or not. Which result in an ugly response 500.
My question: How to avoid that ? The best for me would be to just continue to use "form->isValid()" as before , but if there's an other standard way to handle that, it's okay too.
Note: it would be best if I don't have to put my entity's setter as accepting null values
The exception I got:
Expected argument of type "DateTimeInterface", "NULL" given.
in vendor/symfony/property-acces /PropertyAccessor.php::throwInvalidArgumentException (line 153)
in vendor/symfony/form/Extension/Core/DataMapper/PropertyPathMapper.php->setValue (line 85)
in vendor/symfony/form/Form.php->mapFormsToData (line 622)
in vendor/symfony/form/Extension/HttpFoundation/HttpFoundationRequestHandler.php->submit (line 108)
in vendor/symfony/form/Form.php->handleRequest (line 492)
A curl that reproduce the error :
curl -d 'title=foo' http://127.0.0.1:8080/users/api/tasks
The code :
Entity:
class Task
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="bigint")
*/
private $id;
/**
* #Assert\NotNull()
* #Assert\NotBlank()
* #ORM\Column(type="string", length=500)
*/
private $title;
/**
*
* #ORM\Column(type="datetimetz")
*/
private $dueDatetime;
public function getDueDatetime(): ?\DateTimeInterface
{
return $this->dueDatetime;
}
public function setDueDatetime(\DateTimeInterface $dueDatetime): self
{
$this->dueDatetime = $dueDatetime;
return $this;
}
public function setTitle($title)
{
$this->title = $title;
return $this;
}
public function getTitle()
{
return $this->title;
}
}
Form
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title')
->add('dueDatetime')
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => Task::class]);
}
}
Controller:
class TaskController extends AbstractController
{
/**
* #Route(
* "/users/api/tasks",
* methods={"POST"},
* name="user_api_create_task"
* )
*/
public function apiCreateTask(Request $request)
{
$task = new Task();;
// the use of createNamed with an empty string is just so that
// the external scripts don't have to know about symfony's convention
$formFactory = $this->container->get('form.factory');
$form = $formFactory->createNamed(
'',
TaskType::class,
$task
);
$form->handleRequest($request); // <-- this throw exception
// but this code should handle this no ?
if (!$form->isSubmitted() || !$form->isValid()) {
return new JsonResponse([], 422);
}
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($task);
$entityManager->flush();
return new JsonResponse();
}
}
There are at least 2 ways to handle this.
In the two ways you will have to add #Assert\NotNull() to the dueDatetime attribute.
1 - You can try/catch the exception of the handleRequest call.[edit] this one breaks the flow, not good.
2 - You can make nullable the setter setDueDatetime(\DateTimeInterface $dueDatetime = null). If you choose this one, please be sure to always validate your entity before an Insert/Update in DB else you will get an SQL error.
In the two cases it will be handled by the validator isValid() and you will have a nice error in your front end.
You need to allow nullable parameter (with "?") in method setDueDatetime
public function setDueDatetime(?\DateTimeInterface $dueDatetime): self
{
$this->dueDatetime = $dueDatetime;
return $this;
}
I have a doubt about code organization using symfony3 and doctrine: I'll try to explain as clear as I can. Let's say I have a FootballClub entity:
class FootballClub
{
// other code
private $memberships;
public function addMembership(Membership $membership) : FootballClub
{
$this->memberships[] = $membership;
return $this;
}
public function removeMembership(Membership $membership) : bool
{
return $this->memberships->removeElement($membership);
}
}
The entity is in a many-to-one relationship with another entity, Membership, which represents the contract a player has with the club. Let's say each club
has only a limited number of membership it can acquire, number that is represented as a setting, for example, as a property in a Setting entity.
The question is: how should I reference that setting when removing a membership from the club and check that is respected? Entities should not have any dependency, so what would be the correct way to implement this? A service? can you provide an example? Thank you for your time.
You could create a Settings entity, linked in OneToOne relation with FootballCluc entity.
Define Settings like this and instanciate it in the FootballClub's constructor
Settings entity
/** #Entity */
class Settings
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="integer")
*/
private $maxMembership;
// Other configurable properties ...
__constructor($maxMembership = 50)
{
$this->maxMembership = $maxMembership;
}
public function getMaxMembership()
{
return $this->maxMembership;
}
public function setMaxMembership($maxMembership)
{
$this->maxMembership = $maxMembership;
}
}
Football Entity
class FootballClub
{
/**
* One FootballClub has One Settings.
* #OneToOne(targetEntity="Settings")
* #JoinColumn(name="settings_id", referencedColumnName="id")
*/
private $settings;
// other code
private $memberships;
__constructor(Settings $settings = null)
{
if (null === $settings) {
$settings = new Settings();
}
$this->settings = $settings;
}
public function addMembership(Membership $membership) : FootballClub
{
if ($this->settings->getMaxMembership() <= count($this->memberships)) {
// throw new Exception("Max number of membership reached"); Strict mode
// return false // soft mode
}
$this->memberships-> = $membership;
return $this;
}
public function removeMembership(Membership $membership) : bool
{
return $this->memberships->removeElement($membership);
}
}
i need some help i want to write a unit test about a controler method , i have searched for examples and tested a lot of method's but none of them has worked:
Here is my controller:
class ComputerController extends Controller
{
/**
* #Route("/list-computers.html", name="back_computer_list")
* #return RedirectResponse|Response
*/
function listComputerAction()
{
$ad = $this->get("ldap_service");
$computers = $ad->getAllComputer();
return $this->render('BackBundle:Computer:list.html.twig', array(
"computers" => $computers,
));
}
I have tried to test it with mock like this:
class ComputerController extends Controller
{
/**
* #var EngineInterface
*/
private $templating;
public function setTemplating($templating)
{
$this->templating = $templating;
}
and i have created a test method:
class ComputerControllerTest extends TestCase {
public function testlistComputerAction(){
$templating = $this->getMockBuilder('BackBundle\Controller\ComputerController')->getMock();
$computers = [1,2];
$templating->expects($this->once())
->method('render')
->with('BackBundle:Computer:list.html.twig', array(
"computers" => $computers))
->will($this->returnValue( $computers));
$controller = new ComputerController();
$controller->setTemplating($templating);
$this->assertEquals('success', $controller->listComputerAction());
}
When i start executing phpunit , i have this warning"Trying to configure method "render" which cannot be configured because it does not exist, has not been specified, is final, or is static"
I would be thankful if someone has an idea about this
I tried to Test a method in ldapService : Here is the method's of the service that i want to test
/**
* #return bool|resource
*/
public function getLdapBind()
{
if (!$this->ldapBind) {
if ($this->getLdapConnect()) {
$this->ldapBind = #ldap_bind($this->ldapConnect, $this->ldapUser, $this->ldapPass);
}
}
return $this->ldapBind;
}
/**
* #param $ldapUser
* #param $password
* #return bool
*/
function isAuthorized($ldapUser, $password)
{
$result = false;
if ($this->ldapConnect) {
$result = #ldap_bind($this->ldapConnect, $ldapUser, $password);
}
return $result;
}
Here is the test (using Mock):
<?php
namespace BackBundle\Tests\Service;
use PHPUnit\Framework\TestCase;
use BackBundle\Service\LdapService;
use PHPUnit_Framework_MockObject_InvocationMocker;
class LdapServiceTest extends TestCase {
public function testgetLdapConnect()
{
// $LdapService = new LdapService();
$ldapMock = $this->getMockBuilder( 'LdapService')->setMethods(['getLdapBind'])->disableOriginalConstructor()->getMock();
$ldapMock->expects($this->once())
// ->method()
->with(array('ldap_bind', 'mike', 'password'))
->will($this->returnValue(true));
$ldapMock->isAuthorized('mike', 'password');
}
}
But i have a warning that i can't resolve : "Method name matcher is not defined, cannot define parameter matcher without one"
If someone , has an idea about that please
Honestly, there is nothing useful to test in that three-line controller. #1 is the service container, and #3 is the Twig subsystem. Line #2 can be unit tested on it's own.
With more complex controllers, I have found that making them a service where all the dependencies are passed in, either by constructor, or into the action itself does make slightly more complex controllers quite easy, but very few need that anyway.
I am using Voters to restrict access to entities in a REST API.
Step 1
Consider this voter that restricts users access to blog posts:
class BlogPostVoter extends Voter
{
public function __construct(AccessDecisionManagerInterface $decisionManager)
{
$this->decisionManager = $decisionManager;
}
/**
* Determines if the attribute and subject are supported by this voter.
*
* #param string $attribute An attribute
* #param int $subject The subject to secure, e.g. an object the user wants to access or any other PHP type
*
* #return bool True if the attribute and subject are supported, false otherwise
*/
protected function supports($attribute, $subject)
{
if (!in_array($attribute, $this->allowedAttributes)) {
return false;
}
if (!$subject instanceof BlogPost) {
return false;
}
return true;
}
/**
* Perform a single access check operation on a given attribute, subject and token.
*
* #param string $attribute
* #param mixed $subject
* #param TokenInterface $token
* #return bool
* #throws \Exception
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
return $this->canUserAccess($attribute, $subject, $token);
}
public function canUserAccess($attribute, $subject, TokenInterface $token) {
if ($this->decisionManager->decide($token, array('ROLE_SUPPORT', 'ROLE_ADMIN'))) {
return true;
}
//other logic here omitted ...
return false;
}
}
You can see there is a public function canUserAccess to determine if the user is allowed to see the BlogPost. This all works just fine.
Step 2
Now I have another voter that checks something else, but also needs to check this same exact logic for BlogPosts. My thought was to:
add a new voter
perform some other checks
but then also perform this BlogPost check
So I thought I would inject the BlogPostVoter into my other voter like this:
class SomeOtherVoter extends Voter
{
public function __construct(BlogPostVoter $blogPostVoter)
{
$this->decisionManager = $decisionManager;
}
...
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
//other logic
if ($this->blogPostVoter->canUserAccess($attribute, $subject, $token)) {
return true;
}
return false;
}
}
Problem
When I do this I get the following error, using both setter and constructor injection:
Circular reference detected for service "security.access.decision_manager", path: "security.access.decision_manager"
I don't see where the security.access.decision_manager should depend on the Voter implementations. So I'm not seeing where the circular reference is.
Is there another way I can call VoterA from VoterB?
To reference VoterOne from VoterTwo you can inject the AuthorizationCheckerInterface into VoterTwo and then call ->isGranted('ONE'). Where ONE is the supported attribute of VoterOne.
Like:
class VoterTwo extends Voter
{
private $authorizationChecker;
public function __construct(AuthorizationCheckerInterface $authorizationChecker)
{
$this->authorizationChecker = $authorizationChecker;
}
protected function supports($attribute, $subject)
{
return in_array($attribute, ['TWO']);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
return $this->authorizationChecker->isGranted('ONE', $subject);
}
}
In this example VoterTwo does just redirect the request to VoterOne (or the voter that supports the attribute ONE). This can then be extended through additional conditions.
I have 2 entities, Contact and ContactType.
The owner entity is Contact, with a property $type :
/**
* #ORM\ManyToOne(targetEntity="Evo\BackendBundle\Entity\ContactType")
* #ORM\JoinColumn(nullable=true)
*/
protected $type = null;
I now have to set this relation to be mandatory. I tried the following :
/**
* #ORM\ManyToOne(targetEntity="Evo\BackendBundle\Entity\ContactType")
* #ORM\JoinColumn(nullable=false)
*/
protected $type = 2;
But I get an error, which is pretty logic. I should set an entity (with id 2) as default, not a integer. But I have no idea how to do this. I previously read I shouldn't do any query to DB or any use of EntityManager inside an entity. So how can I set a default ContactType ?
A better solution probably would be to put this logic in some kind of "manager" service, for example a ContactManager.
<?php
use Doctrine\ORM\EntityManagerInterface;
class ContactManager
{
private $manager;
public function __construct(EntityManagerInterface $manager)
{
$this->manager = $manager;
}
public function createContact(ContactType $type = null)
{
if (!$type instanceof ContactType) {
$type = $this->manager->getReference('ContactType', 2);
}
return new Contact($type);
}
}
Then define your service (for example in services.yml):
contact_manager:
class: ContactManager
arguments: [#doctrine.orm.entity_manager]