Two table in one controller - zend-framework3

I want use two model in one controller.
Controlle:
protected $table;
/**
* Execute the request
*
* #param MvcEvent $e
* #return mixed
*/
protected $commentTable;
// Add this constructor:
public function __construct(PostTable $table,CommentTable $commTable)
{
$this->table = $table;
$this->commentTable = $commTable;
}
Factory:
class PostControllerFactory implements FactoryInterface {
public function __invoke(ContainerInterface $container, $requestedName, array $options = null){
$model = $container->get(PostTable::class);
return new PostController($model);
}
}
But I got error:
:__construct() must be an instance of Post\Model\CommentTable, none given,
How use two tables in one controller?

You are passing only PostTable but not CommentTable in your PostController constructor thought that needs to be there while creating factory for your controller. So you should do that in this way
class PostControllerFactory implements FactoryInterface {
public function __invoke(ContainerInterface $container, $requestedName, array $options = null) {
$postTable = $container->get(PostTable::class);
$commentTable = $container->get(CommentTable::class);
return new PostController($postTable, $commentTable);
}
}

Related

When implementing based on Drupal 9 ResourceBase, dependency injection resolution seems to fail

I want to write a RestResource based on ResourceBase. Most of this is working ok, but I wanted to inject a service class to house db/repo logic; in order to do this, I attempted to overload the create // construct methods of ResourceBase, but when I apply the code from core/modules/rest/src/Plugin/ResourceBase.php :
public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->serializerFormats = $serializer_formats;
$this->logger = $logger;}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('rest')
);
}
and then run cache-rebuild, with my module enabled, the logger.factory->rest seems to be returning a LoggerChannel instead of a LoggerInterface:
Uncaught TypeError: Argument 5 passed to Drupal\xxxxx\Plugin\rest\resource\xxxxx::__construct() must be an instance of Drupal\xxxxx\Plugin\rest\resource\LoggerInterface, instance of Drupal\Core\Logger\LoggerChannel given
what is the correct container / service reference for what is required here?
You are probably declaring LoggerInterface's alias wrong.
Replace:
use Drupal\xxxxx\Plugin\rest\resource\LoggerInterface
with:
use Psr\Log\LoggerInterface;
Good point. I re-examined what I had, and added the above using statement. Also checked my services.yml to make sure I used the correct service resolution for the DI (xxx.repository), and had to cross-check all the namespaces
final version (with repo injection below):
namespace Drupal\xxx\Plugin\rest\resource;
use Drupal\xxx;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\Core\Datetime\DrupalDateTime;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Psr\Log\LoggerInterface;
/**
* Provides Resource
*
* #RestResource(
* id = "xxx_resource",
* label = #Translation("XXX Resource"),
* serialization_class = "",
* uri_paths = {
* "canonical" = "/xxx",
* "create" = "/xxx/callback"
* }
* )
*/
class XXXResource extends ResourceBase {
private \Drupal\XXX\Repository $repository;
public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger, \Drupal\XXX\Repository $repo) {
parent::__construct($configuration, $plugin_id, $plugin_definition,$serializer_formats,$logger,$repo);
$this->serializerFormats = $serializer_formats;
$this->logger = $logger;
$this->repository=$repo;
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('rest'),
$container->get('xxx.repository')
);
}
//add post/get / etc behaviors below, then enable via RestUI module

Handling imporper data during deserialization when using Symfony Serializer Component

I am new to the Symfony serializer component. I am trying to properly deserialize a JSON body to the following DTO:
class PostDTO
{
/** #var string */
private $name;
/**
* #return string
*/
public function getName(): string
{
return $this->name;
}
/**
* #param string $name
*/
public function setName(string $name): void
{
$this->name = $name;
}
}
The controller method as follows:
/**
* #Route (path="", methods={"POST"}, name="new_post")
* #param Request $request
* #return Response
*/
public function create(Request $request): Response
{
$model = $this->serializer->deserialize($request->getContent(), PostDTO::class, 'json');
// call the service with the model
return new JsonResponse();
}
My problem is that I wanted to handle business-validation after the body was deserialized. However, if i specify an invalid value for the name, such as false or [], the deserialization will fail with an exception: Symfony\Component\Serializer\Exception\NotNormalizableValueException: "The type of the "name" attribute for class "App\Service\PostDTO" must be one of "string" ("array" given)..
I do understand that it is because I intentionally set "name": []. However, I was looking for a way to set the fields to a default value or even perform some validation pre-deserialization.
I have found the proper way to handle this. That exception was thrown because the serializer was not able to create the PostDTO class using the invalid payload I have provided.
To handle this, I have created my custom denormalizer which kicks in only for this particular class. To do this, I have implemented the DenormalizerInterface like so:
use App\Service\PostDTO;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class PostDTODeserializer implements DenormalizerInterface
{
/** #var ObjectNormalizer */
private $normalizer;
/**
* PostDTODeserializer constructor.
* #param ObjectNormalizer $normalizer
*/
public function __construct(ObjectNormalizer $normalizer)
{
$this->normalizer = $normalizer;
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
return $type === PostDTO::class;
}
/**
* #param mixed $data
* #param string $type
* #param string|null $format
* #return array|bool|object
* #throws ExceptionInterface
*/
public function supportsDenormalization($data, string $type, string $format = null)
{
// validate the array which will be normalized (you should write your validator and inject it through the constructor)
if (!is_string($data['name'])) {
// normally you would throw an exception and leverage the `ErrorController` functionality
// do something
}
// convert the array to the object
return $this->normalizer->denormalize($data, $type, $format);
}
}
If you want to access the context array, you can implement the DenormalizerAwareInterface. Normally, you would create your custom validation and inject it into this denormalizer and validate the $data array.
Please not that I have injected the ObjectNormalizer here so that when the data successfully passed the validation, I can still construct the PostDTO using the $data.
PS: in my case, the autowiring has automatically registered my custom denormalizer. If yours is not autowired automatically, go to services.yaml and add the following lines:
App\Serializer\PostDTODeserializer:
tags: ['serializer.normalizer']
(I have tagged the implementation with serializer.normalizer so as it is recognized during the deserialization pipeline)

Symfony 4.4 Event Listener Error (MongoDB)

I'm trying to create a listener to when a new Rating is created. I followed all the documentation but I keep getting the same error:
Argument 1 passed to "Symfony\Component\EventDispatcher\EventDispatcherInterface::dispatch()" must be an instance of "Symfony\Component\EventDispatcher\Event", "App\Event\AverageRatingEvent" given.
I'm trying to use Symfony\Component\EventDispatcher\Event in the event but it keeps saying that it is deprecated and according to documents to use Symfony\Contracts\EventDispatcher\Event instead.
I register my event in the services and the following is my event, eventlistener and class
Class Rating
class RatingApiController extends AbstractController
{
/**
* #Route("api/rating/create", name="CreateRating", methods={"POST"})
* #param DocumentManager $dm
* #param Request $request
* #param EventDispatcher $eventDispatcher
* #return RedirectResponse|Response
* #throws MongoDBException
*
*/
public function addRating(Request $request, EventDispatcherInterface $eventDispatcher)
{
$response = [];
$form = $this->
createForm(RatingType::class, new Rating() ,array('csrf_protection' => false));
$request = json_decode($request->getContent(), true);
$form->submit($request);
if($form->isSubmitted() && $form->isValid())
{
$rating = $form->getData();
$this->documentManager->persist($rating);
$this->documentManager->flush();
$averageRatingEvent = new AverageRatingEvent($rating);
$eventDispatcher->dispatch( AverageRatingEvent::NAME, $averageRatingEvent);
$status = 200;
$response = ["status" => $status, "success" => true, "data" => $rating->getId()];
// return $this->redirectToRoute('rating_list');
}
}
Event
<?php
namespace App\Event;
use App\Document\Rating;
use Symfony\Contracts\EventDispatcher\Event;
class AverageRatingEvent extends Event
{
/**
* #var Rating $rating
*/
protected $rating;
public const NAME = "average.rating";
public function __construct(Rating $rating)
{
$this->rating = $rating;
}
public function getRating()
{
return $this->rating;
}
}
Listener
<?php
namespace App\Event;
use App\Document\Rating;
use Doctrine\ODM\MongoDB\Event\LifecycleEventArgs;
class AverageRatingListener
{
public function postPersist(LifecycleEventArgs $args)
{
$document = $args->getObject();
if(!$document instanceof Rating)
return;
}
public function RatingCreated()
{
dump("Hello a rating was just added");
}
}
Inside AverageRatingEvent you extend Event.
The use needs to be changed from
use Symfony\Contracts\EventDispatcher\Event;
to
use Symfony\Component\EventDispatcher\Event;

Add PersistentCollection to an object in fixture

I'm trying to create a new fixture for creating a user.
This is the fixture :
class UserFixtures extends Fixture implements DependentFixtureInterface
{
private ManagerRegistry $_managerRegistry;
public function __construct(ManagerRegistry $managerRegistry)
{
$this->_managerRegistry = $managerRegistry;
}
public function load(ObjectManager $manager)
{
$groups = $manager->getRepository(Group::class)->findAll(); // This return an array of object. I want a PersistentCollection
$company = $manager->getRepository(Company::class)->findOneBy(['company_name' => 'HANFF - Global Health Solution']);
$user = new User();
$user->setLogin("TEST_TEST")
->setName("TEST_Name")
->setFirstName("TEST_Firstname")
->setPassword("test")
->setEmail("TEST#hanff.lu");
$user->setCompany($company);
$user->setGroups($groups); // This don't work as it is just an array
$manager->persist($user);
$manager->flush();
}
/**
* #inheritDoc
*/
public function getDependencies()
{
return array(
CompanyFixture::class,
GroupFixture::class
);
}
}
So I have already created the company and group which persist into my database.
And now I want to set to my new user the company and group which have been previously persisted by doctrine.
This work for my company as this is a single object.
But this is not working for my groups as it is typed as a PersistenCollection object and the getRepository(Group::class)->findAll() return an array of object Group.
Here the data contains in the $groups variable :
array:2 [
0 => App\Entity\Group {#1039
-code: "NAT"
-label: "National"
}
1 => App\Entity\Group {#1044
-code: "VET"
-label: "Vétérinaire"
}
]
Here this is how I defined the groups into my User entity :
Class User{
// ...
/**
* #var PersistentCollection
* Many user has many groups
* #ORM\ManyToMany(targetEntity="Group")
* #ORM\JoinTable(name="user_group",
* joinColumns={#ORm\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="group_code", referencedColumnName="`code`")}
* )
*/
private PersistentCollection $groups;
public function getGroups(): PersistentCollection
{
return $this->groups;
}
public function setGroups(PersistentCollection $groups): self
{
$this->groups = $groups;
return $this;
}
public function addGroup($group): self
{
$this->getGroups()->add($group);
return $this;
}
// ...
}
I have read somewhere (can't remember where) that when you persist an object using doctrine, it can be accessed as a PersistentCollection but I can't figure how to do that (Except by creating a new PersistentCollection() which is certainly not the best manner to do it)?
I have tried setting ArrayCollection instead of PersistentCollection, but if I do that, doctrine yells at me when I try to persist my user object because it can't convert ArrayCollection to PersistentCollection (i guess).
You have to change the types of your properties, arguments and return values to the Doctrine\Common\Collections\Collection interface. That's the interface ArrayCollection and PersistentCollection share. Don't forget to initialize your groups property to an ArrayCollection in the constructor. Otherwise calls to addGroup will fail on new user entities.
class User
{
// ...
/**
* #ORM\ManyToMany(targetEntity="Group")
* #ORM\JoinTable(name="user_group",
* joinColumns={#ORm\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="group_code", referencedColumnName="`code`")}
* )
*/
private Collection $groups;
public function __construct()
{
// other initialization
$this->groups = new ArrayCollection();
}
public function getGroups(): Collection
{
return $this->groups;
}
public function setGroups(Collection $groups): self
{
$this->groups = $groups;
return $this;
}
public function addGroup($group): self
{
$this->getGroups()->add($group);
return $this;
}
// ...
}

How to test dependency injection with PHPUnit on symfony 4

In a symfony 4 project, many services/controllers need log.
Trying to use the advantage of traits & autowire options given by symfony, I created a loggerTrait that will be passed to the different services.
namespace App\Helper;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
trait LoggerTrait
{
/** #var LoggerInterface */
private $logger;
/** #var array */
private $context = [];
/**
* #return LoggerInterface
*/
public function getLogger(): LoggerInterface
{
return $this->logger;
}
/**
* #required
*
* #param LoggerInterface|null $logger
*/
public function setLogger(?LoggerInterface $logger): void
{
$this->logger = $logger;
}
public function logDebug(string $message, array $context = []): void
{
$this->log(LogLevel::DEBUG, $message, $context);
}
...
}
(inspired by symfonycasts.com)
A service will be using this trait
namespace App\Service;
use App\Helper\LoggerTrait;
class BaseService
{
use LoggerTrait;
/** #var string */
private $name;
public function __construct(string $serviceName)
{
$this->name = $serviceName;
}
public function logName()
{
$this->logInfo('Name of the service', ['name' => $this->name]);
}
}
It works perfectly but I couldn't succeed to test it.
I tried to extend KernelTestCase in my test to mock an loggerInterface but I receive Symfony\Component\DependencyInjection\Exception\InvalidArgumentException: The "Psr\Log\LoggerInterface" service is private, you cannot replace it which make perfect sens.
Here my test:
namespace App\Tests\Service;
use App\Service\BaseService;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class BaseServiceTest extends KernelTestCase
{
private function loggerMock()
{
return $this->createMock(LoggerInterface::class);
}
protected function setUp()
{
self::bootKernel();
}
/**
* #test
* #covers ::logName
*/
public function itShouldLogName()
{
// returns the real and unchanged service container
$container = self::$kernel->getContainer();
// gets the special container that allows fetching private services
$container = self::$container;
$loggerMock = $this->loggerMock();
$loggerMock->expect(self::once())
->method('log')
->with('info', 'Name of the service', ['name' => 'service_test']);
$this->logger = $container->set(LoggerInterface::class, $loggerMock);
$baseService = new BaseService('service_test');
var_dump($baseService->getLogger());
}
}
Is there a solution to test such a logger inside the service ?
You can override the service to be public (only for the test environment) in your config_test.yml as follows:
services:
Psr\Log\LoggerInterface:
public: true
This is commonly done for testing private services.

Resources