I am trying to work with dataTables according to this tutorial (https://github.com/webinarium/DataTablesBundle/wiki), but I have some troubles:
src/Controller/DataTableController.php
<?php
namespace App\Controller;
use DataTables\DataTablesInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;
/**
*
* #Route("/users", name="users")
*
* #param Request $request
* #param DataTablesInterface $datatables
* #return JsonResponse
*/
class DataTableController extends Controller
{
const ID = 'users';
public function usersAction(Request $request, DataTablesInterface $datatables): JsonResponse
{
try {
// Tell the DataTables service to process the request,
// specifying ID of the required handler.
$results = $datatables->handle($request, 'users');
return $this->json($results);
}
catch (HttpException $e) {
// In fact the line below returns 400 HTTP status code.
// The message contains the error description.
return $this->json($e->getMessage(), $e->getStatusCode());
}
}
}
src/DataTables/UsersDataTable.php
<?php
use DataTables\DataTableHandlerInterface;
use DataTables\DataTableQuery;
use DataTables\DataTableResults;
use Symfony\Bridge\Doctrine\RegistryInterface;
class UsersDataTable implements DataTableHandlerInterface
{
protected $doctrine;
/**
* Dependency Injection constructor.
*
* #param RegistryInterface $doctrine
*/
public function __construct(RegistryInterface $doctrine)
{
$this->doctrine = $doctrine;
}
/**
* {#inheritdoc}
*/
public function handle(DataTableQuery $request): DataTableResults
{
/** #var \Doctrine\ORM\EntityRepository $repository */
$repository = $this->doctrine->getRepository('AppBundle:User');
$results = new DataTableResults();
// Total number of users.
$query = $repository->createQueryBuilder('u')->select('COUNT(u.id)');
$results->recordsTotal = $query->getQuery()->getSingleScalarResult();
// Query to get requested entities.
$query = $repository->createQueryBuilder('u');
// Search.
if ($request->search->value) {
$query->where('(LOWER(u.username) LIKE :search OR' .
' LOWER(u.email) LIKE :search)');
$query->setParameter('search', strtolower("%{$request->search->value}%"));
}
// Filter by columns.
foreach ($request->columns as $column) {
if ($column->search->value) {
$value = strtolower($column->search->value);
// "ID" column
if ($column->data == 0) {
$query->andWhere('u.id = :id');
$query->setParameter('id', intval($value));
}
// "Username" column
elseif ($column->data == 1) {
$query->andWhere('LOWER(u.username) LIKE :username');
$query->setParameter('username', "%{$value}%");
}
// "Email" column
elseif ($column->data == 2) {
$query->andWhere('LOWER(u.email) LIKE :email');
$query->setParameter('email', "%{$value}%");
}
}
}
// Order.
foreach ($request->order as $order) {
// "ID" column
if ($order->column == 0) {
$query->addOrderBy('u.id', $order->dir);
}
// "Username" column
elseif ($order->column == 1) {
$query->addOrderBy('u.username', $order->dir);
}
// "Email" column
elseif ($order->column == 2) {
$query->addOrderBy('u.email', $order->dir);
}
}
// Get filtered count.
$queryCount = clone $query;
$queryCount->select('COUNT(u.id)');
$results->recordsFiltered = $queryCount->getQuery()->getSingleScalarResult();
// Restrict results.
$query->setMaxResults($request->length);
$query->setFirstResult($request->start);
/** #var \AppBundle\Entity\User[] $users */
$users = $query->getQuery()->getResult();
foreach ($users as $user) {
$results->data[] = [
$user->getId(),
$user->getUsername(),
$user->getEmail(),
];
}
return $results;
}
}
config/services.yaml
parameters:
locale: 'en'
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
public: false # Allows optimizing the container by removing unused services; this also means
# fetching services directly from the container via $container->get() won't work.
# The best practice is to be explicit about your dependencies anyway.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
exclude: '../src/{Entity,Migrations,Tests,Kernel.php}'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
datatable.users:
class: AppBundle\DataTables\UsersDataTable
tags: [{ name: datatable, id: users }]
arguments: [ '#doctrine' ]
I get the error message:
You forgot your namespace for your UsersDataTable class file. I think it should be
namespace App\DataTable\UsersDataTable;
Also change the one in services.yaml
Related
I have a service who intercepts the events of Sentry. I'm using a function called beforeSend.
I would to load a json file who contains the data to scrub or to keep. It's a service and I build my constructor with a similar way than others, but the "$this" context doesn't exist when I'm in the debugger in this function.
The kernel is in the Global variables, but I think it's not a good idea... I only would to get the root dir and it's all, but I don't find how to do this in this class... The constructor seems useless.
Someone could help me with a similar experience ?
EDIT :
Service :
namespace App\Services;
use Sentry\Event;
use Symfony\Component\HttpKernel\KernelInterface;
class SentryBeforeSendService
{
private static $rootDir;
public function __construct(KernelInterface $kernel)
{
self::$rootDir = $kernel->getRootDir();
}
/**
* Scrubs the value of all TARGET_PARAMETERS
* in the event's request.
*
* #param Event $event
*
* #return Event
*/
public function beforeSend(Event $event)
{
$rootDir = self::$rootDir;
$event->setRequest(self::scrubRequest($event->getRequest(), $rootDir));
try {
$composerData = json_decode(file_get_contents($rootDir.'/../composer.json'), true);
$version = $composerData['version'];
$event->setRelease($version);
} catch (\Exception $e) {
//do nothing
}
return $event;
}
/**
* Scrubs GET and POST parameters
*
* #param array $request
*
* #return array
*/
private static function scrubRequest(array $request, $rootDir)
{
// DO SOMETHING WITH $rootDir to scrub data with external file
}}
services.yml :
app.service.sentry_before_send:
class: 'App\Services\SentryBeforeSendService'
arguments: ['#kernel']
config_prod.yml :
sentry:
dsn: "%sentry_dsn%"
options:
environment: "%sentry_environment%"
# release: '%env(VERSION)%' #overridden from composer.json version in SentryBeforeSendService::beforeSend
before_send: 'App\Services\SentryBeforeSendService::beforeSend'
But it seems the construct never happened.
Thank you very much.
I was unable to inject a parameter, but I found a way to get the project_root from my method. Half victory ...
config_prod.yml:
sentry:
dsn: "%sentry_dsn%"
options:
environment: "%sentry_environment%"
# release: '%env(VERSION)%' #overridden from composer.json version in SentryBeforeSendService::beforeSend
before_send: 'App\Services\SentryBeforeSendService::beforeSend'
project_root: '%kernel.project_dir%'
Service :
<?php
namespace App\Services;
use Sentry\Event;
use Sentry\State\Hub;
class SentryBeforeSendService
{
private static $projectRoot;
/**
* Scrubs the value of all TARGET_PARAMETERS
* in the event's request.
*
* #param Event $event
*
* #return Event
*/
public function beforeSend(Event $event)
{
$sentryClient = Hub::getCurrent()->getClient();
self::$projectRoot = $sentryClient->getOptions()->getProjectRoot();
$event->setRequest(self::scrubRequest($event->getRequest()));
try {
$composerData = json_decode(file_get_contents(self::$projectRoot.'/composer.json'), true);
$version = $composerData['version'];
$event->setRelease($version);
} catch (\Exception $e) {
//do nothing
}
return $event;
}}
Hope it'll help someone else.
Thank you for answers.
You can inject the kernel.project_dir parameter in your service constructor with a named parameter:
In your services.yml file:
services:
_defaults:
bind:
string $kernelProjectDir: '%kernel.project_dir%'
Then in your service:
public function __construct(string $kernelProjectDir)
{
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 use FOSRestBundle in Symfony 4 to API project. I use annotations and in controller I have for example
use FOS\RestBundle\Controller\Annotations as Rest;
/**
* #Rest\Get("/api/user", name="index",)
* #param UserRepository $userRepository
* #return array
*/
public function index(UserRepository $userRepository): array
{
return ['status' => 'OK', 'data' => ['users' => $userRepository->findAll()]];
}
config/packages/fos_rest.yaml
fos_rest:
body_listener: true
format_listener:
rules:
- { path: '^/api', priorities: ['json'], fallback_format: json, prefer_extension: false }
param_fetcher_listener: true
view:
view_response_listener: 'force'
formats:
json: true
Now I'd like to add custom header 'X-Total-Found' to my response. How to do it?
You are relying in FOSRestBundle ViewListener, so that gives you limited options, like not being able to pass custom headers. In order to achieve what you want, you will need to call $this->handleView() from your controller and pass it a valid View instance.
You can use the View::create() factory method or the controller $this->view() shortcut. Both take as arguments the array of your data, the status code, and a response headers array. Then, you can set up your custom header there, but you will have to do that for every call.
The other option you have, which is more maintainable, is register a on_kernel_response event listener/subscriber and somehow pass it the value of your custom header (you could store it in a request attribute for example).
Those are the two options you have. You may have a third one, but I cannot come up with it at the minute.
I ran into the same issue. We wanted to move pagination meta information to the headers and leave the response without an envelope (data and meta properties).
My Environment
Symfony Version 5.2
PHP Version 8
FOS Rest Bundle
STEP 1: Create an object to hold the header info
// src/Rest/ResponseHeaderBag.php
namespace App\Rest;
/**
* Store header information generated in the controller. This same
* object is used in the response subscriber.
* #package App\Rest
*/
class ResponseHeaderBag
{
protected array $data = [];
/**
* #return array
*/
public function getData(): array
{
return $this->data;
}
/**
* #param array $data
* #return ResponseHeaderBag
*/
public function setData(array $data): ResponseHeaderBag
{
$this->data = $data;
return $this;
}
public function addData(string $key, $datum): ResponseHeaderBag
{
$this->data[$key] = $datum;
return $this;
}
}
STEP 2: Inject the ResponseHeaderBag into the controller action
public function searchCustomers(
ResponseHeaderBag $responseHeaderBag
): array {
...
...
...
// replace magic strings and numbers with class constants and real values.
$responseHeaderBag->add('X-Pagination-Count', 8392);
...
...
...
}
STEP 3: Register a Subscriber and listen for the Response Kernel event
// config/services.yaml
App\EventListener\ResponseSubscriber:
tags:
- kernel.event_subscriber
Subscribers are a great way to listen for events.
// src/EventListener/ResponseSubscriber
namespace App\EventListener;
use App\Rest\ResponseHeaderBag;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class ResponseSubscriber implements EventSubscriberInterface
{
public function __construct(
protected ResponseHeaderBag $responseHeaderBag
){
}
/**
* #inheritDoc
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::RESPONSE => ['addAdditionalResponseHeaders']
];
}
/**
* Add the response headers created elsewhere in the code.
* #param ResponseEvent $event
*/
public function addAdditionalResponseHeaders(ResponseEvent $event): void
{
$response = $event->getResponse();
foreach ($this->responseHeaderBag->getData() as $key => $datum) {
$response->headers->set($key, $datum);
}
}
}
Trying to use the serializer component in Symfony 3.3. I struggle with entities having 'DateTime' members.
My config.yml serializer init:
serializer:
enable_annotations: true
Added this in service.yml:
datetime_method_normalizer:
class: Symfony\Component\Serializer\Normalizer\DateTimeNormalizer
public: false
tags: [serializer.normalizer]
The deserialized code looks like this:
$yml = [...] // It was created by serializer->serialize()
$serializer = $this->get('serializer');
$myObject = $serializer->deserialize($yml, MyObject::class, "yaml");
The error is get is: Expected argument of type "DateTime", "string" given in in vendor/symfony/symfony/src/Symfony/Component/Serializer/Normalizer/AbstractObjectNormalizer.php (line 204)
I think the DateTimeNormalizer::denormalize never gets called. Any idea how to bring it back to life?
Info: DateTimeNormalizer::__constructor() is called.
As DateTime is a nested object, you should use PropertyInfo Component as described here —
https://symfony.com/doc/current/components/serializer.html#recursive-denormalization-and-type-safety
The extraction of property information is performed by extractor
classes.
https://symfony.com/doc/current/components/property_info.html#extractors
There are 4 types of extractors:
ReflectionExtractor
PhpDocExtractor
SerializerExtractor
DoctrineExtractor
For example, using ReflectionExtractor you need to specify type hint for either params or return type. It also looks for constructor params (requires to be enabled explicitly)
class Item {
protected $date;
public function setDate(\DateTime $date) {...}
public function getDate() : \DateTime {...}
}
Property Info is registered automatically when option set:
# config/packages/framework.yaml
framework:
property_info: ~
After that you need to override serializer service to use it, or define a custom one. And the last part — add DateTimeNormalizer, so DateTime can be processed by serializer.
app.normalizer.item_normalizer:
class: Symfony\Component\Serializer\Normalizer\ObjectNormalizer
arguments:
- null
- null
- null
- '#property_info.reflection_extractor'
tags: [ 'serializer.normalizer' ]
app.serializer.item:
class: Symfony\Component\Serializer\Serializer
public: true
arguments:
- [
'#serializer.normalizer.datetime',
'#app.normalizer.item_normalizer',
]
- [ '#serializer.encoder.json' ]
That's it.
This question break my brain recently, and I've two entities with dateTime property, the solution is custom denormalizer like this:
<?php
namespace MyBundle\Serializer\Normalizer;
use MyBundle\Entity\MyEntity1;
use MyBundle\Entity\MyEntity2;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
/**
* DateTime hook normalizer
*/
class DateTimeHookNormalizer implements DenormalizerInterface
{
/**
* {#inheritdoc}
*/
public function denormalize($data, $class, $format = null, array $context = array())
{
if (isset($data['MyDateTime1']) && is_string($data['MyDateTime1']))
{
$data['MyDateTime1'] = new \DateTime($data['MyDateTime1']);
}
if (isset($data['MyDateTime2']) && is_string($data['MyDateTime2']))
{
$data['MyDateTime2'] = new \DateTime($data['MyDateTime2']);
}
And more ...
$normalizer = new ObjectNormalizer();//default normalizer
return $normalizer->denormalize($data, $class, $format, $context);
}
}
/**
* {#inheritdoc}
*/
public function supportsDenormalization($data, $type, $format = null)
{
return is_array($data) && ($type === MyEntity1::class || $type === MyEntity2::class);
}
And declare service like this :
# DateTime Hook Normalizer
Mybundle.normalizer.dateTimeHook:
class: 'MybundleBundle\Serializer\Normalizer\DateTimeHookNormalizer'
public: false
tags: [serializer.normalizer]
It's ok for me, that work !
The only official way is seems to declare a callback:
$callback = function ($dateTime) {
return $dateTime instanceof \DateTime
? $dateTime->format(\DateTime::ISO8601)
: '';
};
$normalizer->setCallbacks(array('createdAt' => $callback));
$serializer = new Serializer(array($normalizer), array($encoder));
https://symfony.com/doc/current/components/serializer.html#using-callbacks-to-serialize-properties-with-object-instances
I'm using the doctrine softdeleteable extension on a project and have my controller action set up as such.
/**
* #Route("address/{id}/")
* #Method("GET")
* #ParamConverter("address", class="MyBundle:Address")
* #Security("is_granted('view', address)")
*/
public function getAddressAction(Address $address)
{
This works great as it returns NotFound if the object is deleted, however I want to grant access to users with ROLE_ADMIN to be able to see soft deleted content.
Does there already exist a way to get the param converter to disable the filter or am I going to have to create my own custom param converter?
There are no existing ways to do it, but I've solved this problem by creating my own annotation, that disables softdeleteable filter before ParamConverter does its job.
AcmeBundle/Annotation/IgnoreSoftDelete.php:
namespace AcmeBundle\Annotation;
use Doctrine\Common\Annotations\Annotation;
/**
* #Annotation
* #Target({"CLASS", "METHOD"})
*/
class IgnoreSoftDelete extends Annotation { }
AcmeBundle/EventListener/AnnotationListener.php:
namespace AcmeBundle\EventListener;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\Common\Annotations\Reader;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class AnnotationListener {
protected $reader;
public function __construct(Reader $reader) {
$this->reader = $reader;
}
public function onKernelController(FilterControllerEvent $event) {
if (!is_array($controller = $event->getController())) {
return;
}
list($controller, $method, ) = $controller;
$this->ignoreSoftDeleteAnnotation($controller, $method);
}
private function readAnnotation($controller, $method, $annotation) {
$classReflection = new \ReflectionClass(ClassUtils::getClass($controller));
$classAnnotation = $this->reader->getClassAnnotation($classReflection, $annotation);
$objectReflection = new \ReflectionObject($controller);
$methodReflection = $objectReflection->getMethod($method);
$methodAnnotation = $this->reader->getMethodAnnotation($methodReflection, $annotation);
if (!$classAnnotation && !$methodAnnotation) {
return false;
}
return [$classAnnotation, $classReflection, $methodAnnotation, $methodReflection];
}
private function ignoreSoftDeleteAnnotation($controller, $method) {
static $class = 'AcmeBundle\Annotation\IgnoreSoftDelete';
if ($this->readAnnotation($controller, $method, $class)) {
$em = $controller->get('doctrine.orm.entity_manager');
$em->getFilters()->disable('softdeleteable');
}
}
}
AcmeBundle/Resources/config/services.yml:
services:
acme.annotation_listener:
class: AcmeBundle\EventListener\AnnotationListener
arguments: [#annotation_reader]
tags:
- { name: kernel.event_listener, event: kernel.controller }
AcmeBundle/Controller/DefaultController.php:
namespace AcmeBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use AcmeBundle\Annotation\IgnoreSoftDelete;
use AcmeBundle\Entity\User;
class DefaultController extends Controller {
/**
* #Route("/{id}")
* #IgnoreSoftDelete
* #Template
*/
public function indexAction(User $user) {
return ['user' => $user];
}
}
Annotation can be applied to individual action methods and to entire controller classes.
You can use #Entity for this, customizing a repository method like this:
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
/**
* #Route("/{id}")
* #Entity("post", expr="repository.findDisableFilter(id)")
*/
public function disable(Post $post): JsonResponse
{
...
}
and then in your repository class:
public function findDisableFilter(mixed $id): mixed
{
$filterName = 'your-filter-name';
$filters = $this->getEntityManager()->getFilters();
if ($filters->has($filterName) && $filters->isEnabled($filterName)) {
$filters->disable($filterName);
}
return $this->find($id);
}