namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints; /** * @ORM\Table(name="projects") * @ORM\Entity * @Constraints\UniqueEntity(fields={"name"}, message=" .") */ class Project { const MAX_NAME = 25; const MAX_DESCRIPTION = 100; /** * @var int ID. * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var string . * * @ORM\Column(name="name", type="string", length=25) */ private $name; /** * @var string . * * @ORM\Column(name="description", type="string", length=100, nullable=true) */ private $description; }
class ProjectForm extends AbstractType { /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('name', TextType::class, [ 'label' => ' ', 'required' => true, 'attr' => ['maxlength' => Project::MAX_NAME], 'constraints' => [ new Constraints\NotBlank(), new Constraints\Length(['max' => Project::MAX_NAME]), ], ]); $builder->add('description', TextType::class, [ 'label' => ' ', 'required' => false, 'attr' => ['maxlength' => Project::MAX_DESCRIPTION], 'constraints' => [ new Constraints\Length(['max' => Project::MAX_DESCRIPTION]), ], ]); } /** * {@inheritdoc} */ public function getBlockPrefix() { return 'project'; } }
namespace AppBundle\Controller; use AppBundle\Entity\Project; use AppBundle\Form\ProjectForm; use Sensio\Bundle\FrameworkExtraBundle\Configuration; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; class ProjectController extends Controller { /** * , "". * * @Configuration\Route("/new") * @Configuration\Method({"GET", "POST"}) */ public function newAction(Request $request) { $project = new Project(); $form = $this->createForm(ProjectForm::class, $project); $form->handleRequest($request); if ($form->isValid()) { $this->getDoctrine()->getManager()->persist($project); $this->getDoctrine()->getManager()->flush(); return $this->redirectToRoute('projects'); } return $this->render('project/form.html.twig', [ 'form' => $form->createView(), ]); } }
namespace AppBundle\Controller; use AppBundle\Entity\Project; use AppBundle\Form\ProjectForm; use Sensio\Bundle\FrameworkExtraBundle\Configuration; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; class ProjectController extends Controller { /** * HTML- . * * @Configuration\Route("/new", condition="request.isXmlHttpRequest()") * @Configuration\Method("GET") */ public function showNewFormAction() { $form = $this->createForm(ProjectForm::class, null, [ 'action' => $this->generateUrl('new_project'), ]); return $this->render('project/form.html.twig', [ 'form' => $form->createView(), ]); } /** * "" . * * @Configuration\Route("/new", name="new_project", condition="request.isXmlHttpRequest()") * @Configuration\Method("POST") */ public function newAction(Request $request) { $project = new Project(); $form = $this->createForm(ProjectForm::class, $project); $form->handleRequest($request); if ($form->isValid()) { $this->getDoctrine()->getManager()->persist($project); $this->getDoctrine()->getManager()->flush(); return new JsonResponse(); } else { $error = $form->getErrors(true)->current(); return new JsonResponse($error->getMessage(), JsonResponse::HTTP_BAD_REQUEST); } } }
{{ form_start(form) }} {{ form_row(form.name) }} {{ form_row(form.description) }} {{ form_end(form) }}
composer require simple-bus/symfony-bridge
command_bus
, which calls the previously registered handlers.catch
section returns the HTTP code "4xx" with a specific error message. Let's see how it looks in business: class ProjectForm extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('name', TextType::class, [ 'label' => 'Project name', 'required' => true, 'attr' => ['maxlength' => Project::MAX_NAME], ]); $builder->add('description', TextType::class, [ 'label' => 'Project description', 'required' => false, 'attr' => ['maxlength' => Project::MAX_DESCRIPTION], ]); } public function getBlockPrefix() { return 'project'; } }
namespace AppBundle\SimpleBus\Project; use Symfony\Component\Validator\Constraints; /** * Create new project. * * @property string $name Project name. * @property string $description Description. */ class CreateProjectCommand { /** * @Constraints\NotBlank() * @Constraints\Length(max = "25") */ public $name; /** * @Constraints\Length(max = "100") */ public $description; }
namespace AppBundle\SimpleBus\Project\Handler; use AppBundle\Entity\Project; use AppBundle\SimpleBus\Project\CreateProjectCommand; use Symfony\Bridge\Doctrine\RegistryInterface; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Validator\Validator\ValidatorInterface; class CreateProjectCommandHandler { protected $validator; protected $doctrine; /** * Dependency Injection constructor. * * @param ValidatorInterface $validator * @param RegistryInterface $doctrine */ public function __construct(ValidatorInterface $validator, RegistryInterface $doctrine) { $this->validator = $validator; $this->doctrine = $doctrine; } /** * Creates new project. * * @param CreateProjectCommand $command * @throws BadRequestHttpException */ public function handle(CreateProjectCommand $command) { $violations = $this->validator->validate($command); if (count($violations) != 0) { $error = $violations->get(0)->getMessage(); throw new BadRequestHttpException($error); } $entity = new Project(); $entity ->setName($command->name) ->setDescription($command->description); $this->doctrine->getManager()->persist($entity); $this->doctrine->getManager()->flush(); } }
command_bus
found by our handler, it must be registered as a service, having marked with a special tag. services: command.project.create: class: AppBundle\SimpleBus\Project\Handler\CreateProjectCommandHandler tags: [{ name: command_handler, handles: AppBundle\SimpleBus\Projects\CreateProjectCommand }] arguments: [ "@validator", "@doctrine" ]
showNewFormAction
function showNewFormAction
not changed at all ( showNewFormAction
omit it for short), only newAction
changed. class ProjectController extends Controller { public function newAction(Request $request) { try { // "project". "$request->request->all()". $data = $request->request->get('project'); $command = new CreateProjectCommand(); $command->name = $data['name']; $command->description = $data['description']; $this->container->get('command_bus')->handle($command); return new JsonResponse(); } catch (\Exception $e) { return new JsonResponse($e->getMessage(), $e->getStatusCode()); } } }
persist($entity)
, an exception will occur in the command handler - it will be created by the ORM itself, adding the message that we specified in the Project
class annotation ( “A project with this name already exists” ). As a result, the action itself has not changed at all - we just catch any exception, no matter what level it occurs, and turn it into an “HTTP 400 ″.handle
function in the command handler, you will notice that validation and processing of its result: namespace AppBundle\SimpleBus\Middleware; use Psr\Log\LoggerInterface; use SimpleBus\Message\Bus\Middleware\MessageBusMiddleware; use Symfony\Component\Validator\Validator\ValidatorInterface; class ValidationMiddleware implements MessageBusMiddleware { protected $logger; protected $validator; /** * Dependency Injection constructor. * * @param LoggerInterface $logger * @param ValidatorInterface $validator */ public function __construct(LoggerInterface $logger, ValidatorInterface $validator) { $this->logger = $logger; $this->validator = $validator; } /** * {@inheritdoc} */ public function handle($message, callable $next) { $violations = $this->validator->validate($message); if (count($violations) != 0) { $error = $violations->get(0)->getMessage(); $this->logger->error('Validation exception', [$error]); throw new BadRequestHttpException($error); } $next($message); } }
services: middleware.validation: class: AppBundle\SimpleBus\Middleware\ValidationMiddleware public: false tags: [{ name: command_bus_middleware }] arguments: [ "@logger", "@validator" ]
class CreateProjectCommandHandler { protected $doctrine; /** * Dependency Injection constructor. * * @param RegistryInterface $doctrine */ public function __construct(RegistryInterface $doctrine) { $this->doctrine = $doctrine; } /** * Creates new project. * * @param CreateProjectCommand $command */ public function handle(CreateProjectCommand $command) { $entity = new Project(); $entity ->setName($command->name) ->setDescription($command->description); $this->doctrine->getManager()->persist($entity); $this->doctrine->getManager()->flush(); } }
class ValidationException extends BadRequestHttpException { protected $messages = []; /** * {@inheritdoc} */ public function __construct(array $messages, $code = 0, \Exception $previous = null) { $this->messages = $messages; parent::__construct(count($messages) ? reset($this->messages) : '', $previous, $code); } /** * @return array */ public function getMessages() { return $this->messages; } }
class ValidationMiddleware implements MessageBusMiddleware { public function handle($message, callable $next) { $violations = $this->validator->validate($message); if (count($violations) != 0) { $errors = []; foreach ($violations as $violation) { $errors[$violation->getPropertyPath()] = $violation->getMessage(); } $this->logger->error('Validation exception', $errors); throw new ValidationException($errors); } $next($message); } }
catch
section): class ProjectController extends Controller { public function newAction(Request $request) { try { $data = $request->request->get('project'); $command = new CreateProjectCommand(); $command->name = $data['name']; $command->description = $data['description']; $this->container->get('command_bus')->handle($command); return new JsonResponse(); } catch (ValidationException $e) { return new JsonResponse($e->getMessages(), $e->getStatusCode()); } catch (\Exception $e) { return new JsonResponse($e->getMessage(), $e->getStatusCode()); } } }
{ "name": " .", "description": " 100 ." }
$.ajax({ // ... error: function(xhr) { var response = xhr.responseJSON ? xhr.responseJSON : xhr.responseText; if (typeof response === 'object') { $.each(response, function(id, message) { var name = $('form').prop('name'); var $control = $('#' + name + '_' + id); if ($control.length === 0) { alert(message); } else { $control.after('<p class="form-error">' + message + '</p>'); } }); } else { alert(response); } }, beforeSend: function() { $('.form-error').remove(); } });
trait MessageTrait { /** * . * * @param array $values . */ public function __construct(array $values = []) { foreach ($values as $property => $value) { if (property_exists($this, $property)) { $this->$property = $value; } } } }
class ProjectController extends Controller { public function newAction(Request $request) { try { $data = $request->request->get('project'); $command = new CreateProjectCommand($data); $this->container->get('command_bus')->handle($command); return new JsonResponse(); } catch (ValidationException $e) { return new JsonResponse($e->getMessages(), $e->getStatusCode()); } catch (\Exception $e) { return new JsonResponse($e->getMessage(), $e->getStatusCode()); } } }
editAction
will obviously be another parameter - the project ID. And it is obvious that the corresponding command will be one more property: /** * Update specified project. * * @property int $id Project ID. * @property string $name New name. * @property string $description New description. */ class UpdateProjectCommand { /** * @Constraints\NotBlank() */ public $id; /** * @Constraints\NotBlank() * @Constraints\Length(max = "25") */ public $name; /** * @Constraints\Length(max = "100") */ public $description; }
trait MessageTrait { /** * . * * @param array $values . * @param array $extra . * . */ public function __construct(array $values = [], array $extra = []) { $data = $extra + $values; foreach ($data as $property => $value) { if (property_exists($this, $property)) { $this->$property = $value; } } } }
editAction
might look like this: class ProjectController extends Controller { /** * HTML- . * * @Configuration\Route("/edit/{id}", requirements={"id"="\d+"}, condition="request.isXmlHttpRequest()") * @Configuration\Method("GET") */ public function showEditFormAction($id) { $project = $this->getDoctrine()->getRepository(Project::class)->find($id); if (!$project) { throw $this->createNotFoundException(); } $form = $this->createForm(ProjectForm::class, $project, [ 'action' => $this->generateUrl('edit_project'), ]); return $this->render('project/form.html.twig', [ 'form' => $form->createView(), ]); } /** * "" . * * @Configuration\Route("/edit/{id}", name="edit_project", requirements={"id"="\d+"}, condition="request.isXmlHttpRequest()") * @Configuration\Method("POST") */ public function editAction(Request $request, $id) { try { $project = $this->getDoctrine()->getRepository(Project::class)->find($id); if (!$project) { throw $this->createNotFoundException(); } $data = $request->request->get('project'); $command = new UpdateProjectCommand($data, ['id' => $id]); $this->container->get('command_bus')->handle($command); return new JsonResponse(); } catch (ValidationException $e) { return new JsonResponse($e->getMessages(), $e->getStatusCode()); } catch (\Exception $e) { return new JsonResponse($e->getMessage(), $e->getStatusCode()); } } }
NULL
database. Expand our trait a little more: trait MessageTrait { public function __construct(array $values = [], array $extra = []) { $empty2null = function ($value) use (&$empty2null) { if (is_array($value)) { foreach ($value as &$v) { $v = $empty2null($v); } return $value; } return is_string($value) && strlen($value) === 0 ? null : $value; }; $data = $empty2null($extra + $values); foreach ($data as $property => $value) { if (property_exists($this, $property)) { $this->$property = $value; } } } }
NULL
.event_bus
.simple_bus
and event_bus
are normal symfony services, you can embed them as dependencies anywhere, including in your handlers. For example, for a project creation team to send an event that a new project was created.Symfony\Bundle\FrameworkBundle\Test\WebTestCase
. At the same time, in any case, we no longer need to use a symfony crawler (which, by the way, slows down the tests noticeably) in order to trigger this or that action. Honestly, now sometimes I don’t even cover “actions” with tests, unless I check them for availability, as recommended by the Symfony documentation.Source: https://habr.com/ru/post/280512/
All Articles