Repository usage in configureActionButtons (Sonata Admin Bundle) - symfony

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();

Related

Custom GET operation for API platform generates wrong documentation

I have added the following operation under TeachingClass entity.
App\Entity\TeachingClass:
collectionOperations:
# ...
itemOperations:
# ...
get_learning_skills:
method: GET
path: /auth/v1/teaching-class/{id}/learning-skills
resourceClass: 'App\Entity\LearningSkill' # Doesn't seem to work
controller: App\Controller\Api\LearningSkillApiController
normalization_context:
groups: ['learning_skill_list']
security: 'is_granted("HAS_TEACHING_CLASS_ACCESS", object)'
swagger_context:
summary: "Retrieves the collection of LearningSkill resources belonging to a specific TeachingClass."
description: "LearningSkills belonging to a specific TeachingClass"
The end-point correctly returns a collection of LearningSkill entities by the configured controller:
<?php
namespace App\Controller\Api;
use App\Entity\LearningSkill;
use App\Entity\TeachingClass;
use App\Repository\LearningSkillRepository;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
/**
* Class LearningSkillApiController.
*/
class LearningSkillApiController
{
private $learningSkillRepository;
public function __construct(LearningSkillRepository $learningSkillRepository)
{
$this->learningSkillRepository = $learningSkillRepository;
}
public function __invoke(TeachingClass $data)
{
return $this->byTeachingClass($data);
}
private function byTeachingClass(TeachingClass $teachingClass)
{
return $this->learningSkillRepository->findByTeachingClass($teachingClass);
}
}
However, my problem is that the generated API doc is wrong:
How do I make the documentation reflect that the response is a collection of LearningSkill entities (instead of a TeachingClass entity)?
I had the same problem with the report in the chapter9-api branch of my tutorial, which outputs instances of DayTotalsPerEmployee instead of the class the endpoint is on. My solution was to make a SwaggerDecorator. Below is one adapted for your operation.
It also sets the descriptions in components schemas referred to by the response 200 content. This is based on the assumption that your response is a collection response. It apip thinks it is an item response there may be some more work to to to make the swagger docs describe a collection response.
<?php
namespace App\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class SwaggerDecorator implements NormalizerInterface
{
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
$this->decorated = $decorated;
}
public function normalize($object, string $format = null, array $context = [])
{
$summary = 'The collection of LearningSkill resources belonging to a specific TeachingClass.';
$docs = $this->decorated->normalize($object, $format, $context);
$docs['paths']['/auth/v1/teaching-class/{id}/learning-skills']['get']['responses']['200']['description'] = 'LearningSkills collection response';
$responseContent = $docs['paths']['/auth/v1/teaching-class/{id}/learning-skills']['get']['responses']['200']['content'];
$this->setByRef($docs, $responseContent['application/ld+json']['schema']['properties']['hydra:member']['items']['$ref'],
'description', $summary);
$this->setByRef($docs, $responseContent['application/json']['schema']['items']['$ref'],
'description', $summary);
return $docs;
}
public function supportsNormalization($data, string $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
private function setByRef(&$docs, $ref, $key, $value)
{
$pieces = explode('/', substr($ref, 2));
$sub =& $docs;
foreach ($pieces as $piece) {
$sub =& $sub[$piece];
}
$sub[$key] = $value;
}
}
To configure the service add the following to api/config/services.yaml:
'App\Swagger\SwaggerDecorator':
decorates: 'api_platform.swagger.normalizer.api_gateway'
arguments: [ '#App\Swagger\SwaggerDecorator.inner' ]
autoconfigure: false

Drupal: "Error: Class not found" when calling a function from a controller within submitForm()

I'm trying to do some stuff while submitting a form in a custom module. Some of that is done by calling a function from a controller. That's when i get:
Error: Class 'Drupal\ice_cream\Controller\OrderController' not found in Drupal\ice_cream\Form\OrderForm->submitForm() (line 77 of modules\custom\ice_cream\src\Form\OrderForm.php).
As far as I can tell the namespaces aren't wrong? Or is that not related to this error?
This is how my OrderForm.php and submitForm() looks like:
<?php
namespace Drupal\ice_cream\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\ice_cream\Controller\OrderController;
/**
* Implements the order form.
*/
class OrderForm extends FormBase {
... (omitted code for getFormid and buildForm)
public function submitForm(array &$form, FormStateInterface $form_state) {
//Check if the order is ice or waffles.
if($form_state->getValue('foodType') == 'ice'){
//Save order to the DB.
OrderController::saveOrder($form_state->getValue('foodType'), $form_state->getValue('taste'));
... (more code)
}
}
}
This is how the controller looks like:
<?php
namespace Drupal\ice_cream\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Database;
/**
* Order controller for interacting (insert, select,...) with the ice cream table in the DB.
*/
class OrderController extends ControllerBase {
/**
* Saves an order for either Ice or Waffles with their options (tast or toppings).
*/
public function saveOrder($foodType, $options) {
$connection = Database::getConnection();
//Check if ice or waffles (to only insert the right field with $options).
if($foodType == "ice"){
$result = $connection->insert('ice_cream')
->fields([
'foodType' => $foodType,
'taste' => $options,
'toppings' => "",
])
->execute();
return true;
}elseif($foodType == "waffles"){
$result = $connection->insert('ice_cream')
->fields([
'foodType' => $foodType,
'taste' => "",
'toppings' => $options,
])
->execute();
return true;
}
}
}
Try using the below code:
$obj= new OrderController;
$obj->saveOrder($form_state->getValue('foodType'), $form_state->getValue('taste'));
Solved:
Just solved it with my mentor. The code was more or less correct, still needed to make my functions static in the OrderController and also I made a stupid error of forgetting the .php extension in my filename when I created it with the 'touch' terminal command...

Dynamic EntityManager find () method returns "table not found" when a custom repository is in use

First I will explain why and how the solution works and then the problems I have encountered. If you think there is a better way to do what I do, I'd love to hear it. I would also like to know why doctrine behaves in this way.
It turns out that my aplication needs to connect to a different database according to the client. I have a table, in a fixed database, containing the connection information that is used in some request.
I have had success with the following code:
class DynamicEntityManager {
protected $em;
private $request;
private $client_id;
public function __construct(RequestStack $request, EntityManagerInterface $em){
$this->em = $em;
$this->request = $request;
}
public function getEntityManager(ClientConn $client = null) {
$request = $this->request->getCurrentRequest();
if($client == NULL){
$domain = $request->attributes->get('domain');
if($domain == "" || $domain == NULL){
throw new \Exception("Error de conexion", 1);
}
$client = $this->em->getRepository(ClientConn::class)->findOneBy(array(
"subdomain" => $domain
));
if($client == NULL){
throw new \Exception("Error de conexion", 1);
}
}
$connectionDB = $client->getConnection();
$dbdriver = 'oci8';
$conexionSplit = explode(':',$connectionDB);
$dbhost = $conexionSplit[0];
$dbport = $conexionSplit[1];
$dbname = $conexionSplit[2];
$dbuser = $client->getUsuarioBd();
$dbpass = $client->getClaveBd();
$service = false;
$this->client_id = $client->getId();
if(strpos($dbname,'SN=') !== false){
$parts = explode('=',$dbname);
$dbname = $parts[1];
$service = true;
}
$request->attributes->set('client_id',$client->getId());
$conn = array(
'driver' => $dbdriver,
'host' => $dbhost,
'port' => $dbport,
'dbname' => $dbname,
'user' => $dbuser,
'password' => $dbpass,
'service' => $service,
'charset' => 'UTF8',
'schema' => null
);
return EntityManager::create($conn, $this->em->getConfiguration());
}
}
As you can see I return EntityManager::create($conn, $this->em->getConfiguration ()) with the new connection. The way I use it is the next:
/**
* #Route("/api/client/{id}/conf/{confID}", name="conf.show")
* #Method({"GET"})
*/
public function show(ClientConn $client, Request $request, DynamicEntityManager $dem ,$confId){
try {
$em = $dem->getEntityManager($client);
$entity = $em->getRepository(Configuration::class)->find($confId);
return new JsonResponse($entity, 200);
}
catch(\Exception $ex) {
return new JsonResponse([
"excepcion" => $ex->getMessage()
], $ex->getCode());
}
}
It works as expected or so I believed until I saw that when the entity has a custom repository it is unable to use the dynamic connection and therefore the previous route will return a table not found exception.
#ORM\Entity() <-- Works like a charm
#ORM\Entity(repositoryClass="App\Repository\ConfigurationRepository")<-- Table not found.
It works in the repository if I create the connection again, although I do not like the solution. So, what do I want? I would like to be able to use the basic methods like find (), findBy () and others without having to rewrite them every time I use a custom repository.
class ConfigurationRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry, DynamicEntityManager $dem)
{
parent::__construct($registry, Configuration::class);
$this->dem= $dem;
}
public function uglyFind($client, $confID)
{
$query = $this->dem->getEntityManager($client)->createQueryBuilder('conf')
->select("conf")
->from(ConfPedidosLentes::class,'conf')
->where('conf.id = :value')->setParameter('value', $confID)
->getQuery();
return $query->getOneOrNullResult();
}
I will really appreciate any contribution and thought in this matter.
Instead of:
class ConfigurationRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry, DynamicEntityManager $dem)
{
parent::__construct($registry, Configuration::class);
$this->dem= $dem;
}
...
try extending EntityRepository (without using a constructor) and use find as you did in your controller:
use Doctrine\ORM\EntityRepository;
class ConfigurationRepository extends EntityRepository
{
}
ServiceEntityRepository is an optional EntityRepository base class with a simplified constructor for autowiring, that explicitly sets the entity manager to the EntityRepository base class. Since you have not configured your doctrine managers to handle these connections properly (it's not even possible actually with so many connections), ServiceEntityRepository will pass a wrong EntityManager instance to the EntityRepository subclass, that's why you should not extend ServiceEntityRepository but EntityRepository.

PHP using interfaces to browse multiple data source

I am creating a Symphony application that browses different data sources.
the controller that I created knows too much about the data source but the application is designed in a way to not expect that.
The data source could be DB, JSON or XML.
is there any way to implement interfaces to do that?
My controller knows the location of the XML file, and browse different data seperatly. I want to do it in one action.
That's my current controller ;
public function searchAction(Request $request) {
if ($request->getMethod() == 'POST') {
$search_for = $request->get('search');
//getting the searched products from the database
$repository = $this->getDoctrine()->getRepository('TyreTyreBundle:Products');
$query = $repository->createQueryBuilder('u')
->where("u.name LIKE '%".$search_for."%' or u.manufacturer LIKE '%".$search_for."%'")
->getQuery();
$results = $query->getResult();
//adding the XML file products
$file_url = "bundles/tyretyre/xml/products.xml";
//Convert the products.XML file into a SimpleXMLElement object
$simpleXMLElementObject = simplexml_load_file($file_url);
$i=0;
//the array where will saved the searched products from the XML file
$xml_result = [];
//looping the xml object to find matching results
while ($simpleXMLElementObject->product[$i]) {
//first we will convert to lower case both searched item and the tested name
if (strstr(strtolower($simpleXMLElementObject->product[$i]->name),strtolower($search_for))){
//push that element into the array to display it later in the twig file
array_push($xml_result, $simpleXMLElementObject->product[$i]);
}
$i++;
}
//end of products searching from the XML source
//display the detail page with passing the DB result and XML result arrays
return $this->render('TyreTyreBundle:Default:detail.html.twig', array('results' => $results,'xml_result' => $xml_result));
}
return $this->render('TyreTyreBundle:Default:search.html.twig');
}
My products entity :
namespace Tyre\TyreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Products
*/
class Products
{
//some getter and setters and private attribute
}
EDIT following VolCh solution,
I registered the service as following (I think I am doing it wrong) in /src/Tyre/TyreBundle/Resources/config/services.yml:
services:
Tyre\TyreBundle\Repository\DoctrineProductRepository:
class: Tyre\TyreBundle\Repository\DoctrineProductRepository
Tyre\TyreBundle\Repository\ProductRepositoryInterface:
class: Tyre\TyreBundle\Repository\ProductRepositoryInterface
But then I get the following
ContextErrorException: Notice: Undefined index: in
/home/smiles/Documents/tyre/src/Tyre/TyreBundle/Controller/DefaultController.php
line 56
Line 56: is this line : $serviceName = $repositoryMap[$request->get('db')];
You could:
declare \TyreTyreBundle\ProductRepository interface (or ProductDataSource if you wish) with method ->search(string $needle): array (or some DTO)
implement it in DoctrinreProductRepository, XmlProductRepository, JsonProductRepository as service with constructor injection of \Doctrine\EntityRepository, xml-filename, json-filename
get properly repository from container in actions
(optional) create ProductRepositoryFactory whith createFor('db|xml|json') method and pass type to controller as part of route like '/datasource/{db|xml|json}' or request parameter like datasource?type=db'and create properly repository in one common action
Added:
Example (proof of concept, don't use, php7+):
src/TyreBundle/Repository/ProductRepositoryInterface.php
namespace Tyre\TyreBundle\Repository;
interface ProductRepositoryInterface
{
function search(string $needle): array;
}
src/TyreBundle/Repository/DoctrineProductRepository.php
namespace Tyre\TyreBundle\Repository;
class DoctrineProductRepository implements ProductRepositoryInterface
{
public function __constructor(EntityManager $em)
{
$this->em = $em;
}
public function search(string $needle): array
{
$repository = $this->em->getRepository('TyreTyreBundle:Products');
$query = $repository->createQueryBuilder('u')
->where("u.name LIKE '%".$needle."%' or u.manufacturer LIKE '%".$needle."%'")
->getQuery();
return $query->getArrayResult();
}
}
src/TyreBundle/Repository/XmlProductRepository.php
src/TyreBundle/Repository/JsonProductRepository.php
controller
public function searchAction(Request $request)
{
$repositoryMap = [
'db' => DoctrineProductRepository::class,
'xml' => XmlProductRepository::class,
'json' => JsonProductRepository::class,
];
$serviceName = $repositoryMap[$request->get('type')];
/** #var ProductRepositoryInterface */
$repository = $this->get($serviceName);
$results = $repository->search($request->get('serxh_for'));
return $this->render('TyreTyreBundle:Default:detail.html.twig', array('results' => $results));
}
also you should register Repository classes as services with their names.

Symfony3 - Serializing nested entities

So I have a couple doctrine entities, a Subscription and a Subscriber. There are many Subscriptions to a single subscriber (manyToOne). I wrote custom normalizers for both entities, but am having trouble getting the Subscriber to show up in the Subscription once it has been normalized to JSON.
The only way I've been able to get it to work is by passing the 'Subscriber' normalizer to the 'Subscription' normailizer. It seems like I should just be able to use the SerializerAwareNormalizer Trait, or something like that, to have Symfony recursively normalize my related entities.
services:
acme.marketing.api.normalizer.subscription:
class: acme\MarketingBundle\Normalizer\SubscriptionNormalizer
arguments: ['#acme.marketing.api.normalizer.subscriber']
public: false
tags:
- { name: serializer.normalizer }
acme.marketing.api.normalizer.subscriber:
class: acme\MarketingBundle\Normalizer\SubscriberNormalizer
public: false
tags:
- { name: serializer.normalizer }
and the normalizer...
<?php
namespace acme\MarketingBundle\Normalizer;
use acme\MarketingBundle\Entity\Subscription;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class SubscriptionNormalizer implements NormalizerInterface
{
private $subscriberNormalizer;
public function __construct($subscriberNormalizer)
{
$this->subscriberNormalizer = $subscriberNormalizer;
}
public function normalize($subscription, $format = null, array $context = [])
{
/* #var $subscription Subscription */
$subscriber = $subscription->getSubscriber();
return [
"id" => $subscription->getId(),
"subscriber" => $this->subscriberNormalizer->normalize($subscriber, $format)
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof Subscription;
}
}
Is there a better way to accomplish this?
Spent a few hours on google and couldn't figure it out. Post on SO and 5 minutes later hit the right google link :(. Answer seems to be to implement NormalizerAwareInterface on the custom normalizer, and then use the NormalizerAwareTrait to get access to the normalizer for nested entities.
<?php
namespace acme\MarketingBundle\Normalizer;
use acme\MarketingBundle\Entity\Subscription;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class SubscriptionNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
public function normalize($subscription, $format = null, array $context = [])
{
return [
"id" => $subscription->getId(),
"subscriber" => $this->normalizer->normalize($subscription->getSubscriber())
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof Subscription;
}
}

Resources