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.
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 manage to get a filtered collection of my Note entities with API Platform, using the #ApiFilter(SearchFilter::class) annotation.
Now I want to convert the json response which is an hydra collection
Example :
{
"#context": "/api/contexts/Note",
"#id": "/api/notes",
"#type": "hydra:Collection",
"hydra:member": []
}
to an archive containing one file by Note and return its metadata.
Example :
{
"name": "my_archive.zip",
"size": 12000,
"nb_of_notes": 15
}
I want to keep the SearchFilter benefits. Is the Normalization the good way to go ?
How to declare the normalizer ? How to access the collection/array of Notes in my normalize() method ?
According to the documentation symfony custom_normalizer , you can create a custom normalizer for your Note entity (for example NoteNormalizer). In the supportsNormalization method your must precise that the normalizer will only affect your Note entity by providing Note entity class. So in the normalize method, you will get each item of your ArrayCollection of Note. If you want to be sure, you can make a dump to $data variable (dd($data)) inside this normalize method, and you will have the first element of you ArrayCollection.
that's how I tried to understand it.
namespace App\Serializer;
use App\Entity\Note;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class NoteNormalizer implements ContextAwareNormalizerInterface
{
private $normalizer;
public function __construct(ObjectNormalizer $normalizer) // Like in documentation you can inject here some customer service or symfony service
{
$this->normalizer = $normalizer;
}
public function normalize($topic, $format = null, array $context = [])
{
$data = $this->normalizer->normalize($topic, $format, $context);
$data['name'] = 'some name';
$data['size'] = 12000;
$data['nb_of_notes'] = 15;
return $data;
}
public function supportsNormalization($data, $format = null, array $context = [])
{
return $data instanceof Note;
}
}
Or if you want you can use this command to generate it automatically :
php bin/console make:serializer:normalizer
And give the name : NoteNormalizer
Simply create a "collection Normalizer" :
note: works the same for vanilla symfony projects too.
namespace App\Serializer;
use App\Entity\Note;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
class NoteCollectionNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
public function supportsNormalization($data, $format = null, array $context = []): bool
{
if(!is_array($data) || (!current($data) instanceof Note)) {
return false;
}
return true;
}
/**
* #param Note[] $collection
*/
public function normalize($collection, $format = null, array $context = [])
{
// ...
}
}
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
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...
Hi I want to render a form in symFony2 with dynamic fields added to it.
So I have wrote code in controller as:
public function getDataAction(){
$handler = new AnotherClass();
$object = $handler->getForm($getAdditionalData);
}
and "AnotherClass" defined as following:
class AnotherClass extends Controller implements requiredInterface{
public function getForm($formData){
//Here i want to write Logic to render a dynamic form with dynamic fields
$form = $this->createFormBuilder($formData)
->setAction($this->generateUrl('me_route_go'))
// Set form field of additional data
foreach ($formData as $k => $v) {
$form->add($k, 'text');
}
//Create form and submit button
$form = $form->add('submit', 'submit')->add('Cancel', 'reset')->getForm();
$form = $form->getForm();
}
}
}
But here I am getting following error:
Error: Call to a member function get() on a non-object.
return $this->container->get('form.factory')->createBuilder($type, $data, $options);
Please suggest what could be the issue.
thanks in advance..
Your controller AnotherClass requires the Dependency Injection Container since you are extending the base Controller class, you have to set it after you instantiate it :
public function getDataAction(){
$handler = new AnotherClass();
$handler->setContainer($this->container);
$object = $handler->getForm($getAdditionalData);
}
You can also create it as a service :
services.yml
name.of.your.service:
class: Path\To\AnotherClass
calls:
- [setContainer, [ "#service_container" ]]
And then :
public function getDataAction(){
$handler = $this->get('name.of.your.service');
$object = $handler->getForm($getAdditionalData);
}