
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