I would like to decorate the ArrayDenormalizer for the Symfony serializer with a custom Denormalizer, but it seems it hits a Circular reference problem in the DependencyInjection since Nginx is crashing with a 502.
The custom Denormalizer implements DenormalizerAwareInterface so i actually expected that Symfony would handle the dependency injection automatically via Autowiring.
<?php
namespace App\Serializer;
use App\MyEntity;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
class PreCheckRequestDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
public function denormalize(mixed $data, string $type, string $format = null, array $context = [])
{
if (in_array($data['locale'], ['de', 'en']) === false) {
$data['locale'] = 'en';
}
return $this->denormalizer->denormalize($data, $type, $format, $context);
}
public function supportsDenormalization(mixed $data, string $type, string $format = null)
{
return is_array($data) && $type === MyEntity::class;
}
}
What am i missing here?
Btw it is Symfony 6.1.
Seems this is a bug with autowiring of NormalizeAwareInterface in Symfony 6.1:
https://github.com/symfony/maker-bundle/issues/1252#issuecomment-1342478104
This bug led into a circular reference problem.
I solved it with not using the DenormalizerAwareInterface and DenormalizerAwareTrait and by disabling autowiring for the custom Denormalizer and declare the service explicitly:
App\Serializer\PreCheckRequestDenormalizer:
autowire: false
arguments:
$denormalizer: '#serializer.normalizer.object'
$allowedLocales: '%app.allowed_locales%'
$defaultLocale: '%env(string:DEFAULT_LOCALE)%'
The comments of the issue describe possible further other ways, but thsi solves it for me.
Related
I need an advice for my Symfony 3.4 project (yes I know there is Symfony 6), I have an entity Product and I created a method that calculates the completion percentage of a Product sheet. It checks many properties of the entity and some other entities related to it (collections).
For now, I placed that method inside my Product entity and it works well. But for a specific thing, I need to do a complex query to the database and I can't use the query builder in the Entity class. So I'm wondering if I should place that code in the ProductController or maybe in the ProductRepository ?
Is it possible to use the entity object in the repository ? I don't need to build queries for each check, I simply use entity getters for the most of the checks.
Then, I will show the result in several pages of my project.
My function is someting like this (simplified) :
public function checkSetup()
{
$setup = array(
'active' => $this->isActive(),
'ref' => !empty($this->ref) ? true : false,
'tags' => $this->tags->isEmpty() ? false : true,
);
// I want to add the following part :
$qb = $em->getRepository(Product::class)->createQueryBuilder('p');
// build complex query...
$records = $qb->getQuery()->getResult();
$setup['records'] = !empty($records) ? false : true;
// Completion level
$score = 0;
foreach ($setup as $s) {
if ($s) $score++;
}
$num = $score / count($setup) * 100;
$setup['completion'] = round($num);
return $setup;
}
Based on the comment of #Cerad :
Create a specific class for the checker.
<?php
// src/Checker/ProductChecker.php
namespace AppBundle\Checker;
use AppBundle\Entity\Product\Product;
use Doctrine\ORM\EntityManager;
class ProductChecker
{
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function check(Product $p)
{
// code...
$result = $this->em->getRepository(Product::class)->customQuery($p->getId());
// code...
}
}
Register ProductChecker as a service and inject EntityManager for using ProductRepository in ProductChecker.
# app/config/services.yml
app.product_checker:
public: true
class: AppBundle\Checker\ProductChecker
arguments: ["#doctrine.orm.entity_manager"]
Write the complex query in ProductRepository.
Then, call the checker in the Controller :
$checker = $this->get("app.product_checker");
$report = $checker->check($product);
I am trying to inject doctrine service (if there is a way to inject BoothTypeRepository without Doctrine, that's also fine) into configureActionButtons and I can't find the way to inject nor doctrine or BoothTypeRepository.
public function configureActionButtons($action, $object = null): array
{
$list = parent::configureActionButtons($action, $object);;
$handles = $this->getConfigurationPool()->getContainer()->get('doctrine.orm.entity_manager')->getRepository(BoothTypeRepository::class)->findAll();
foreach($handles as $handle) {
$list['new_' . $handle] = [
'attr' => $handle,
'template' => 'CRUD/button_new_booth_type.html.twig'
];
}
unset($list['new']);
return $list;
}
The Error which I got from the upper code
An exception has been thrown during the rendering of a template
("Class "App\Repository\BoothTypeRepository" sub class of
"Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository" is
not a valid entity or mapped super class.").
This is the namespace for BoothTypeRepository -> namespace App\Repository;
Answer to this is in construct:
private BoothTypeRepository $boothTypeRepository;
public function __construct($code, $class, $baseControllerName, BoothTypeRepository $boothTypeRepository)
{
parent::__construct($code, $class, $baseControllerName);
$this->boothTypeRepository = $boothTypeRepository;
}
Then one don't need to use doctrine service ->
$boothTypes = $this->boothTypeRepository->findAll();
I've been wondering if it's possible to use the JMS Serializer to deserialize JSON into an existing object.
Usually that would be useful for updating an existing object with new data that you have in a JSON format. Symfony's standard deserializer seems to offer that, but I can't seem to find anything about this with JMS. Have to use JMS though if I want the serializedName Annotation option.
The "workaround" is to deserialize and then use Doctrine's EntityManager to merge, but that only works so well, and you can't easily discern which fields are updated if the JSON doesn't contain every single field.
I have struggled to find the solution but finally found it and here we go:
your services.yaml
jms_serializer.object_constructor:
alias: jms_serializer.initialized_object_constructor
jms_serializer.initialized_object_constructor:
class: App\Service\InitializedObjectConstructor
arguments: ["#jms_serializer.unserialize_object_constructor"]
create class App\Service\InitializedObjectConstructor.php
<?php
declare(strict_types=1);
namespace App\Service;
use JMS\Serializer\Construction\ObjectConstructorInterface;
use JMS\Serializer\DeserializationContext;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Visitor\DeserializationVisitorInterface;
class InitializedObjectConstructor implements ObjectConstructorInterface
{
private $fallbackConstructor;
/**
* #param ObjectConstructorInterface $fallbackConstructor Fallback object constructor
*/
public function __construct(ObjectConstructorInterface $fallbackConstructor)
{
$this->fallbackConstructor = $fallbackConstructor;
}
/**
* {#inheritdoc}
*/
public function construct(
DeserializationVisitorInterface $visitor,
ClassMetadata $metadata,
$data,
array $type,
DeserializationContext $context
): ?object {
if ($context->hasAttribute('target') && 1 === $context->getDepth()) {
return $context->getAttribute('target');
}
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
}
in your controller or your service file
$object = $this->entityManager->find('YourEntityName', $id);
$context = new DeserializationContext();
$context->setAttribute('target', $object);
$data = $this->serializer->deserialize($request->getContent(), 'YourEntityClassName', 'json', $context);
So this can be done, I haven't fully worked out how, but I switched away from JMS again, so just for reference, since I guess it's better than keeping the question open for no reason:
https://github.com/schmittjoh/serializer/issues/79 and you might find more digging around the GitHub too.
I'm quiet new to symfony and spend hours trying to find a solution.
I'm building a multilingual website where page slugs are differents.
For example :
www.mywebsite.com/products EN will be www.mywebsite.com/produits FR but both use the same controller
I have to build a dynamic route and here is the way I did I'm pretty sure I can do better, could you help me?
<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class websiteController{
public function __construct(){
$this -> route = array(
'about' => 'page_about',
'contact' => 'page_contact',
);
}
/**
* #Route("/{page}", name="page")
*/
public function pageAction($page)
{
if($page == $this -> route['about']){
return new Response('<html><body>page about</body></html>');
}
if($page == $this -> route['contact']){
return new Response('<html><body>page contact</body></html>');
}
}
}
?>
There is a bundle for routing internationalization called BeSimpleI18nRoutingBundle but it is not available for symfony 4 right now.
Core symfony implementation
With core symfony you could use multiple routes for each controller, that would have a {_locale} parameter with default value, here the problem would be that two different URLs would be returning the same page.
e.g /test would be the same as /test/en
This might cause problems with SEO
here how the annotations would look like if you wish to implement this method
/**
* #Route("/test/{_locale}", defaults={"_locale"="en"}, requirements={"_locale":"en"}, name="default_en")
* #Route("/δοκιμή/{_locale}", defaults={"_locale"="el"}, requirements={"_locale":"el"}, name="default_el", options = {"utf8": true})
* #Route("/tester/{_locale}", defaults={"_locale"="fr"}, requirements={"_locale":"fr"}, name="default_fr")
*/
public function test($_locale)
{
return new Response("Your current locale is : $_locale");
}
Dynamic Route
Another option is to create a Routing Service that would apply your logic.
Here is an example.
this would be the controller that handles all the paths
Controller
namespace App\Controller;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use App\Service\Router;
class RouterController extends Controller {
/**
* #Route("/{path}", name="router", requirements={"path" = ".+"})
*/
public function router($path,Request $request,Router $router) {
$result=$router->handle($path);
if($result){
$result['args']['request']=$request;
return $this->forward($result['class'], $result['args']);
}
throw $this->createNotFoundException('error page not found!');
}
}
This Controller Action depends on a service called Router so you will have to create a Router service that would return the an array (you can change it to return a custom object) with keys class and args that would be used to forward the request to a controller action.
Service
/src/Service/Router.php
Here you should implement a function called handle and you can apply any logic to it
here is a basic example
namespace App\Service;
class Router
{
public function handle($path)
{
switch ($path) {
case "test":
return [
"class" => "App\Controller\TestController::index",
"args" => [
"locale" => 'en'
]
];
case "tester":
return [
"class" => "App\Controller\TestController::index",
"args" => [
"locale" => 'fr'
]
];
default:
return false;
}
}
}
The code above would forward the request to TestController::index function and will add as parameter to that function the locale variable and also it will include the Request object
You could store the routes in a yaml file or database or any other location you like. You can manipulate the $path variable to extract information about id, page etc.
I'm trying to figure that problem for a long day.
I have Sonata User Bundle installed and I use it to manage users and profile edition and so on.
BUT, I a need to override Sonata Profile Controller.
If I understood correctly (I'm a perfect beginner in Symfony), I have to extend SonataUserBundle (which has been done using the easy extend bundle).
So, when I declare a new controller, nothing happens. Not even an error message.
Any ideas ?
Here are my files
[ BUNDLE EXTENSION FILE ]
// ApplicationSonataUserBundle.php
namespace Application\Sonata\UserBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class ApplicationSonataUserBundle extends Bundle
{
/**
* {#inheritdoc}
*/
public function getParent()
{
return 'SonataUserBundle';
}
}
[ CONTROLLER FILE ]
namespace Application\Sonata\UserBundle\Controller;
use Sonata\UserBundle\Controller\ProfileFOSUser1Controller as BaseController;
class ProfileFOSUser1Controller extends BaseController {
public function editProfileAction() {
die('toto');
$user = $this->getUser();
if (!is_object($user) || !$user instanceof UserInterface) {
throw new AccessDeniedException('This user does not have access to this section.');
}
$form = $this->get('sonata.user.profile.form');
$formHandler = $this->get('sonata.user.profile.form.handler');
$process = $formHandler->process($user);
if ($process) {
$this->setFlash('sonata_user_success', 'profile.flash.updated');
return $this->redirect($this->generateUrl('sonata_user_profile_edit'));
}
return $this->render('SonataUserBundle:Profile:edit_profile.html.twig', array(
'form' => $form->createView(),
'breadcrumb_context' => 'user_profile',
));
}
}
You have die('toto'); as first line in the controller method, isn't that going to terminate all code below?
Ok so I have found the answer by myself :)
I forgot to specify the classes I needed
use FOS\UserBundle\Model\UserInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Sonata\UserBundle\Controller\ProfileFOSUser1Controller as BaseController;
I cleared the cache and everything was ok !
Thanks all !