I'm trying to get this simple notification service working and I am having no joy at all. I've never used services in symfony before so I could be overlooking something pretty basic, however it all seems correct to me so I'm kind of banging my head against a wall here.
I've included everything to do with the service, help would be really appreciated!
Stack Trace:
[1] Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: You have requested a non-existent service "game.notify".
at n/a
in D:\web\www\mygame\app\bootstrap.php.cache line 1968
at Symfony\Component\DependencyInjection\Container->get('game.notify')
in D:\web\www\mygame\vendor\symfony\symfony\src\Symfony\Bundle\FrameworkBundle\Controller\Controller.php line 252
at Symfony\Bundle\FrameworkBundle\Controller\Controller->get('game.notify')
in D:\web\www\mygame\src\Game\MainBundle\Controller\PageController.php line 10
at Game\MainBundle\Controller\PageController->indexAction()
in line
at call_user_func_array(array(object(PageController), 'indexAction'), array())
in D:\web\www\mygame\app\bootstrap.php.cache line 2843
at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), '1')
in D:\web\www\mygame\app\bootstrap.php.cache line 2817
at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), '1', true)
in D:\web\www\mygame\app\bootstrap.php.cache line 2946
at Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel->handle(object(Request), '1', true)
in D:\web\www\mygame\app\bootstrap.php.cache line 2248
at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
in D:\web\www\mygame\web\app_dev.php line 28
Notify Controller:
Located at: Game/MainBundle/Controller/NotifyController.php
<?php
namespace Game\MainBundle\Controller;
class NotifyController
{
private $defaults
= array(
"type" => "flash",
),
$flashes = array();
/**
* #param \Symfony\Component\HttpFoundation\Session\Session $session
*/
public function __construct(\Symfony\Component\HttpFoundation\Session\Session $session)
{
$this->session = $session;
}
/**
* Depending on the supplied type argument, add the values
* to the session flashBag or $this->flashes
*
* #param string $name
* #param array $arguments
*/
public function add($name, array $arguments = array())
{
$arguments += $this->defaults;
// If the type is flash then add the values to the session flashBag
if ($arguments["type"] === "flash") {
$this->session->getFlashBag()->add($name, $arguments);
}
// Otherwise if its instant then add them to the class variable $flashes
elseif ($arguments["type"] === "instant") {
// We want to be able to have multiple notifications of the same name i.e "success"
// so we need to add each new set of arguments into an array not overwrite the last
// "success" value set
if (!isset($this->flashes[$name])) {
$this->flashes[$name] = array();
}
$this->flashes[$name][] = $arguments;
}
}
/**
* Check the flashBag and $this->flashes for existence of $name
*
* #param $name
*
* #return bool
*/
public function has($name)
{
if($this->session->getFlashBag()->has($name)){
return true;
} else {
return isset($this->flashes[$name]);
}
}
/**
* Search for a specific notification and return matches from flashBag and $this->flashes
*
* #param $name
*
* #return array
*/
public function get($name)
{
if($this->session->getFlashBag()->has($name) && isset($this->flashes[$name])){
return array_merge_recursive($this->session->getFlashBag()->get($name), $this->flashes[$name]);
} elseif($this->session->getFlashBag()->has($name)) {
return $this->session->getFlashBag()->get($name);
} else {
return $this->flashes[$name];
}
}
/**
* Merge all flashBag and $this->flashes values and return the array
*
* #return array
*/
public function all()
{
return array_merge_recursive($this->session->getFlashBag()->all(), $this->flashes);
}
}
NotifyExtension.php
Located at: Game/MainBundle/DependencyInjection/NotifyExtension.php
<?php
namespace Game\MainBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
/**
* This is the class that loads and manages your bundle configuration
*
* To learn more see {#link http://symfony.com/doc/current/cookbook/bundles/extension.html}
*/
class NotifyExtension extends Extension
{
/**
* {#inheritDoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
}
Configuration.php
Located at: Game/MainBundle/DependencyInjection/Configuration.php
<?php
namespace Game\MainBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
/**
* This is the class that validates and merges configuration from your app/config files
*
* To learn more see {#link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}
*/
class Configuration implements ConfigurationInterface
{
/**
* {#inheritDoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('game_main');
// Here you should define the parameters that are allowed to
// configure your bundle. See the documentation linked above for
// more information on that topic.
return $treeBuilder;
}
}
Services.yml
Located at: Game/MainBundle/Resources/Config/services.yml
parameters:
game.notify.class: Game\MainBundle\Controller\NotifyController
services:
game.notify:
class: "%game.notify.class%"
arguments:
session: #session
PageController.php
Located at: Game/MainBundle/Controller/PageController.php
<?php
namespace Game\MainBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class PageController extends Controller
{
public function indexAction()
{
$notify = $this->get("game.notify");
$notify->add("test", array("type" => "instant", "message" => "This is awesome"));
if ($notify->has("test")) {
return array("notifications" => $notify->get("test"));
}
return $this->render('GameMainBundle:Page:index.html.twig');
}
}
Based on your answer to my first comment, it would appear that your services are never being loaded due to not following the naming convention for your extension class.
If you have a GameMainBundle for your bundle then you should have GameMainExtension for your extension.
More info here: http://symfony.com/doc/current/cookbook/bundles/best_practices.html
You might still have some problems once you get services.yml loaded. Calling your service a controller is a bit non-standard. But see what happens.
Related
I'm trying to do a hard thing: implementing cache invalidation with Symfony 4.4.13 using FOSHttpCacheBundle 2.9.0 and built-in Symfony reverse proxy.
Unfortunately, I can't use other caching solution (like Varnish or Nginx) because my hosting service doesn't offer them. So, the Symfony built-in reverse proxy is the only solution I have.
I've installed and configured FOSHttpCacheBundle (following the documentation). Also created a CacheKernel class and modified Kernel to use it (following Symfony official documentation, FOSHttpCache documentation and FOSHttpCacheBundle documentation).
After few tests (with my browser), the HTTP caching works and GET responses are cached (seen in browser network analyzer). But, when I update a resource with PUT/PATCH/POST, the GET responses still come from the cache and are unchanged until the expiration. My deduction is the invalidation doesn't work.
Have I do something wrong? Can you help me to troubleshoot?
See my code and configuration below.
config/packages/fos_http_cache.yaml
fos_http_cache:
cache_control:
rules:
-
match:
path: ^/
headers:
cache_control:
public: true
max_age: 15
s_maxage: 30
etag: "strong"
cache_manager:
enabled: true
invalidation:
enabled: true
proxy_client:
symfony:
tags_header: My-Cache-Tags
tags_method: TAGPURGE
header_length: 1234
purge_method: PURGE
use_kernel_dispatcher: true
src/CacheKernel.php
<?php
namespace App;
use FOS\HttpCache\SymfonyCache\CacheInvalidation;
use FOS\HttpCache\SymfonyCache\CustomTtlListener;
use FOS\HttpCache\SymfonyCache\DebugListener;
use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
use FOS\HttpCache\SymfonyCache\PurgeListener;
use FOS\HttpCache\SymfonyCache\RefreshListener;
use FOS\HttpCache\SymfonyCache\UserContextListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
use Symfony\Component\HttpKernel\HttpCache\Store;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class CacheKernel extends HttpCache implements CacheInvalidation
{
use EventDispatchingHttpCache;
// Overwrite constructor to register event listeners for FOSHttpCache.
public function __construct(HttpKernelInterface $kernel, SurrogateInterface $surrogate = null, array $options = [])
{
parent::__construct($kernel, new Store($kernel->getCacheDir()), $surrogate, $options);
$this->addSubscriber(new CustomTtlListener());
$this->addSubscriber(new PurgeListener());
$this->addSubscriber(new RefreshListener());
$this->addSubscriber(new UserContextListener());
if (isset($options['debug']) && $options['debug'])
$this->addSubscriber(new DebugListener());
}
// Made public to allow event listeners to do refresh operations.
public function fetch(Request $request, $catch = false)
{
return parent::fetch($request, $catch);
}
}
src/Kernel.php
<?php
namespace App;
use FOS\HttpCache\SymfonyCache\HttpCacheAware;
use FOS\HttpCache\SymfonyCache\HttpCacheProvider;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
use Symfony\Component\Routing\RouteCollectionBuilder;
class Kernel extends BaseKernel implements HttpCacheProvider
{
use MicroKernelTrait;
use HttpCacheAware;
private const CONFIG_EXTS = '.{php,xml,yaml,yml}';
public function __construct(string $environment, bool $debug)
{
parent::__construct($environment, $debug);
$this->setHttpCache(new CacheKernel($this));
}
...
public/index.php
<?php
use App\Kernel;
use Symfony\Component\ErrorHandler\Debug;
use Symfony\Component\HttpFoundation\Request;
require dirname(__DIR__).'/config/bootstrap.php';
...
$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);
$kernel = $kernel->getHttpCache();
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
One of mine controller, src/Controller/SectionController.php (NOTE: routes are defined in YAML files)
<?php
namespace App\Controller;
use App\Entity\Section;
use App\Entity\SectionCollection;
use App\Form\SectionType;
use FOS\HttpCacheBundle\Configuration\InvalidateRoute;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\View\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class SectionController extends AbstractFOSRestController
{
/**
* List all sections.
*
* #Rest\View
* #param Request $request the request object
* #return array
*
* Route: get_sections
*/
public function getSectionsAction(Request $request)
{
return new SectionCollection($this->getDoctrine()->getRepository(Section::class)->findAll());
}
/**
* Get a single section.
*
* #Rest\View
* #param Request $request the request object
* #param int $id the section id
* #return array
* #throws NotFoundHttpException when section not exist
*
* Route: get_section
*/
public function getSectionAction(Request $request, $id)
{
if (!$section = $this->getDoctrine()->getRepository(Section::class)->find($id))
throw $this->createNotFoundException('Section does not exist.');
return array('section' => $section);
}
/**
* Get friends of the section's user.
*
* #Rest\View
* #return array
*
* Route: get_friendlysections
*/
public function getFriendlysectionsAction()
{
return $this->get('security.token_storage')->getToken()->getUser()->getSection()->getMyFriends();
}
private function processForm(Request $request, Section $section)
{
$em = $this->getDoctrine()->getManager();
$statusCode = $em->contains($section) ? Response::HTTP_NO_CONTENT : Response::HTTP_CREATED;
$form = $this->createForm(SectionType::class, $section, array('method' => $request->getMethod()));
// If PATCH method, don't clear missing data.
$form->submit($request->request->get($form->getName()), $request->getMethod() === 'PATCH' ? false : true);
if ($form->isSubmitted() && $form->isValid()) {
$em->persist($section);
$em->flush();
$response = new Response();
$response->setStatusCode($statusCode);
// set the 'Location' header only when creating new resources
if ($statusCode === Response::HTTP_CREATED) {
$response->headers->set('Location',
$this->generateUrl(
'get_section', array('id' => $section->getId()),
true // absolute
)
);
}
return $response;
}
return View::create($form, Response::HTTP_BAD_REQUEST);
}
/**
*
* Creates a new section from the submitted data.
*
* #Rest\View
* #return FormTypeInterface[]
*
* #InvalidateRoute("get_friendlysections")
* #InvalidateRoute("get_sections")
*
* Route: post_section
*/
public function postSectionsAction(Request $request)
{
return $this->processForm($request, new Section());
}
/**
* Update existing section from the submitted data.
*
* #Rest\View
* #param int $id the section id
* #return FormTypeInterface[]
* #throws NotFoundHttpException when section not exist
*
* #InvalidateRoute("get_friendlysections")
* #InvalidateRoute("get_sections")
* #InvalidateRoute("get_section", params={"id" = {"expression"="id"}})")
*
* Route: put_section
*/
public function putSectionsAction(Request $request, $id)
{
if (!$section = $this->getDoctrine()->getRepository(Section::class)->find($id))
throw $this->createNotFoundException('Section does not exist.');
return $this->processForm($request, $section);
}
/**
* Partially update existing section from the submitted data.
*
* #Rest\View
* #param int $id the section id
* #return FormTypeInterface[]
* #throws NotFoundHttpException when section not exist
*
* #InvalidateRoute("get_friendlysections")
* #InvalidateRoute("get_sections")
* #InvalidateRoute("get_section", params={"id" = {"expression"="id"}})")
*
* Route: patch_section
*/
public function patchSectionsAction(Request $request, $id)
{
return $this->putSectionsAction($request, $id);
}
/**
* Remove a section.
*
* #Rest\View(statusCode=204)
* #param int $id the section id
* #return View
*
* #InvalidateRoute("get_friendlysections")
* #InvalidateRoute("get_sections")
* #InvalidateRoute("get_section", params={"id" = {"expression"="id"}})")
*
* Route: delete_section
*/
public function deleteSectionsAction($id)
{
$em = $this->getDoctrine()->getManager();
if ($section = $this->getDoctrine()->getRepository(Section::class)->find($id)) {
$em->remove($section);
$em->flush();
}
}
}
After searching few days, I found the solution by myself.
In CacheKernel, I extend Symfony\Component\HttpKernel\HttpCache\HttpCache as described in FOSHttpCache documentation. But, the class must extend Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache instead as described in Symfony documentation. By consequences, the constructor change too.
To be honest, I don't know the difference between these two classes but you must use the second one if you want to have a built-in functional reverse proxy. It works now for me.
I put here the final code of src/CacheKernel.php:
<?php
namespace App;
use FOS\HttpCache\SymfonyCache\CacheInvalidation;
use FOS\HttpCache\SymfonyCache\CustomTtlListener;
use FOS\HttpCache\SymfonyCache\DebugListener;
use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
use FOS\HttpCache\SymfonyCache\PurgeListener;
use FOS\HttpCache\SymfonyCache\RefreshListener;
use FOS\HttpCache\SymfonyCache\UserContextListener;
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class CacheKernel extends HttpCache implements CacheInvalidation
{
use EventDispatchingHttpCache;
/**
* Overwrite constructor to register event listeners for FOSHttpCache.
*/
public function __construct(HttpKernelInterface $kernel)
{
parent::__construct($kernel, $kernel->getCacheDir());
$this->addSubscriber(new CustomTtlListener());
$this->addSubscriber(new PurgeListener());
$this->addSubscriber(new RefreshListener());
$this->addSubscriber(new UserContextListener());
if (isset($options['debug']) && $options['debug'])
$this->addSubscriber(new DebugListener());
}
/**
* Made public to allow event listeners to do refresh operations.
*
* {#inheritDoc}
*/
public function fetch(Request $request, $catch = false)
{
return parent::fetch($request, $catch);
}
}
The rest of the code don't change.
Hope it helps. See you.
Having two related entities, let's say Author and Book, I can limit (or paginate) the results of Authors but not the number of results of its related entity Books which always shows the whole collection.
The issue is that Authors may have hundreds of Books making the resulting JSON huge and heavy to parse so I'm trying to get, for example, only the last 5 books.
I'm sure I'm missing something since I think this is probably a common scenario but I can't find anything on the docs nor here in StackOverflow.
I'm starting with Api Platform, any hint would be appreciated!
I finally solved it creating a normalizer for the entity but I still think that it has to be a simpler solution.
Here's what I had to do, following the Authors / Books example:
Add a setter to the Author entity to override the Author's Book collection:
// src/Entity/Author.php
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
// ...
/**
* #ApiResource
* #ORM\Entity(repositoryClass="App\Repository\AuthorRepository")
*/
class Author
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $name;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Book", mappedBy="author", orphanRemoval=true)
*/
private $books;
public function __construct()
{
$this->books = new ArrayCollection();
}
// Getters and setters
//...
public function setBooks($books): self
{
$this->books = $books;
return $this;
}
}
Create a normalizer for the Author's entity:
// App/Serializer/Normalizer/AuthorNormalizer.php
<?php
namespace App\Serializer\Normalizer;
use ApiPlatform\Core\Api\IriConverterInterface;
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;
class AuthorNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface
{
use SerializerAwareTrait;
private $normalizer;
public function __construct(
NormalizerInterface $normalizer,
IriConverterInterface $iriConverter
) {
if (!$normalizer instanceof DenormalizerInterface) {
throw new \InvalidArgumentException('The normalizer must implement the DenormalizerInterface');
}
if (!$normalizer instanceof AbstractItemNormalizer) {
throw new \InvalidArgumentException('The normalizer must be an instance of AbstractItemNormalizer');
}
$handler = function ($entity) use ($iriConverter) {
return $iriConverter->getIriFromItem($entity);
};
$normalizer->setMaxDepthHandler($handler);
$normalizer->setCircularReferenceHandler($handler);
$this->normalizer = $normalizer;
}
public function denormalize($data, $class, $format = null, array $context = [])
{
return $this->normalizer->denormalize($data, $class, $format, $context);
}
public function supportsDenormalization($data, $type, $format = null)
{
return $this->normalizer->supportsDenormalization($data, $type, $format);
}
public function normalize($object, $format = null, array $context = [])
{
// Number of desired Books to list
$limit = 2;
$newBooksCollection = new ArrayCollection();
$books = $object->getBooks();
$booksCount = count($books);
if ($booksCount > $limit) {
// Reverse iterate the original Book collection as I just want the last ones
for ($i = $booksCount; $i > $booksCount - $limit; $i--) {
$newBooksCollection->add($books->get($i - 1));
}
}
// Setter previously added to the Author entity to override its related Books
$object->setBooks($newBooksCollection);
$data = $this->normalizer->normalize($object, $format, $context);
return $data;
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof \App\Entity\Author;
}
}
And finally register the normalizer as a service manually (using autowire led me to circular reference issues):
services:
App\Serializer\Normalizer\AuthorNormalizer:
autowire: false
autoconfigure: true
arguments:
$normalizer: '#api_platform.jsonld.normalizer.item'
$iriConverter: '#ApiPlatform\Core\Api\IriConverterInterface'
I'm using jms/serializer-bundle 2.4.3 on a symfony 4.2 and a I noticed an annoying problem in my application :
when I post an entity, the DoctrineObjectConstructor uses id in content to retrieve another entity and thus patch it while it is excluded by my security groups
see rather entity
class Entity
{
/**
* #var int
*
* #ORM\Column(name="id", type="int")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
* #Serializer\Groups({"GetEntity"})
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string")
* #Serializer\Groups({"GetEntity", "PostEntity"})
*/
private $name;
}
controller
/**
* #Route("/entity", name="post_entity", methods={"POST"})
*/
public function postEntity(Request $request, EntityManagerInterface $entityManager, SerializerInterface $serializer): JsonResponse
{
$deserializationContext = DeserializationContext::create();
$deserializationContext->setGroups(['PostEntity']);
$entity = $serializer->deserialize($request->getContent(), Entity::class, 'json', $deserializationContext);
$entityManager->persist($entity);
$entityManager->flush();
return $this->json($entity, Response::HTTP_OK, [], ['groups' => ['GetEntity']]);
}
I have some JMS configurations changes in services
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
public: true
jms_serializer.unserialize_object_constructor:
class: App\Serializer\ObjectConstructor
If anyone can explain to me how to ignore the id in this case I'm open to any suggestions.
Regards and thanks for any help
To resolve, just add override in your services.yaml
jms_serializer.doctrine_object_constructor:
class: App\Serializer\DoctrineObjectConstructor
arguments:
- '#doctrine'
- '#jms_serializer.unserialize_object_constructor'
jms_serializer.object_constructor:
alias: jms_serializer.doctrine_object_constructor
and add a local DoctrineObjectConstructor updated to ignore entities without current deserialization group on id property
class DoctrineObjectConstructor implements ObjectConstructorInterface
{
const ON_MISSING_NULL = 'null';
const ON_MISSING_EXCEPTION = 'exception';
const ON_MISSING_FALLBACK = 'fallback';
private $fallbackStrategy;
private $managerRegistry;
private $fallbackConstructor;
/**
* Constructor.
*
* #param ManagerRegistry $managerRegistry Manager registry
* #param ObjectConstructorInterface $fallbackConstructor Fallback object constructor
* #param string $fallbackStrategy
*/
public function __construct(ManagerRegistry $managerRegistry, ObjectConstructorInterface $fallbackConstructor, $fallbackStrategy = self::ON_MISSING_NULL)
{
$this->managerRegistry = $managerRegistry;
$this->fallbackConstructor = $fallbackConstructor;
$this->fallbackStrategy = $fallbackStrategy;
}
/**
* {#inheritdoc}
*/
public function construct(VisitorInterface $visitor, ClassMetadata $metadata, $data, array $type, DeserializationContext $context)
{
// Locate possible ObjectManager
$objectManager = $this->managerRegistry->getManagerForClass($metadata->name);
if (!$objectManager) {
// No ObjectManager found, proceed with normal deserialization
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
// Locate possible ClassMetadata
$classMetadataFactory = $objectManager->getMetadataFactory();
if ($classMetadataFactory->isTransient($metadata->name)) {
// No ClassMetadata found, proceed with normal deserialization
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
// Managed entity, check for proxy load
if (!\is_array($data)) {
// Single identifier, load proxy
return $objectManager->getReference($metadata->name, $data);
}
// Fallback to default constructor if missing identifier(s)
$classMetadata = $objectManager->getClassMetadata($metadata->name);
$identifierList = [];
foreach ($classMetadata->getIdentifierFieldNames() as $name) {
$propertyGroups = [];
if ($visitor instanceof AbstractVisitor) {
/** #var PropertyNamingStrategyInterface $namingStrategy */
$namingStrategy = $visitor->getNamingStrategy();
$dataName = $namingStrategy->translateName($metadata->propertyMetadata[$name]);
$propertyGroups = $metadata->propertyMetadata[$name]->groups;
} else {
$dataName = $name;
}
if (!array_key_exists($dataName, $data) || true === empty(array_intersect($context->getAttribute('groups'), $propertyGroups))) {
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
}
$identifierList[$name] = $data[$dataName];
}
// Entity update, load it from database
$object = $objectManager->find($metadata->name, $identifierList);
if (null === $object) {
switch ($this->fallbackStrategy) {
case self::ON_MISSING_NULL:
return null;
case self::ON_MISSING_EXCEPTION:
throw new ObjectConstructionException(sprintf('Entity %s can not be found', $metadata->name));
case self::ON_MISSING_FALLBACK:
return $this->fallbackConstructor->construct($visitor, $metadata, $data, $type, $context);
default:
throw new InvalidArgumentException('The provided fallback strategy for the object constructor is not valid');
}
}
$objectManager->initializeObject($object);
return $object;
}
}
We have a legacy app which is not based on symfony. Doctrine is in use and now we would like to add validation to the models. Seems that the Annotations never get autoloaded, even when "use" statements are in use.
[Semantical Error] The annotation "#Symfony\Component\Validator\Constraints\NotBlank" in property Test\Stackoverflow\User::$Username does not exist, or could not be auto-loaded.
Wrote a small demo application to showcase the problem and how we create the entity manager and validation instance.
composer.json:
{
"require": {
"symfony/validator" : "~3.1"
, "doctrine/orm" : "~2.6.1"
}
}
index.php
require_once ('vendor/autoload.php');
// Load Entities, would normally be done over composer since they reside in a package
require_once('test/User.php');
require_once('MyAnnotationTestApp.php');
// create test app
$app = new MyAnnotationsTestApp();
$app->initEntityManager('localhost', 'annotation_test', 'root', 'mysql', 3306);
if(key_exists('test', $_GET)){
// Create entity and validate it
$entity = new \Test\Stackoverflow\User();
$entity->setUsername('StackoverflowUser');
if($app->testAnnotationWithoutLoading($entity)){
print "Seems the validation was working without preloading the asserts\n<br>";
}
if($app->testAnnotationWithLoading($entity)){
print "Seems the validation was working because we loaded the required class ourself.\n<br>";
}
print "\n<br><br>The question is why the required annotation classes never get autoloaded?";
}else{
// Load the validator class otherwise the annotation throws an exception
$notBlankValidator = new \Symfony\Component\Validator\Constraints\NotBlank();
print "We have cerated the tables but also had to load the validator class ourself.\n<br>\n<br>";
// create tables and
$app->updateDatabaseSchema();
print sprintf('Now lets run the test', $_SERVER['REQUEST_URI']);
}
Doctrine user Entity
<?php
namespace Test\Stackoverflow;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity()
* #ORM\Table(name="users")
*
*/
class User{
/**
* #ORM\Id
* #ORM\Column(name="Id",type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $Id;
public function getId(){
return $this->Id;
}
/**
* #ORM\Column(type="text", length=80, nullable=false)
* #Assert\NotBlank()
*/
protected $Username;
/**
* #return string
*/
public function getUsername()
{
return $this->Username;
}
/**
* #param string $Username
*/
public function setUsername($Username)
{
$this->Username = $Username;
}
}
Demo App with doctrine/validator initialisation:
<?php
final class MyAnnotationsTestApp {
/**
* #var \Doctrine\ORM\EntityManager
*/
private $entityManager;
/**
* #param string $host
* #param string $database
* #param string $username
* #param string $password
* #param integer $port
* #param array $options
* #return \Doctrine\ORM\EntityManager
*/
public function initEntityManager($host, $database, $username, $password, $port, array $options=null){
if($this->entityManager){
return $this->entityManager;
}
$connectionString = sprintf('mysql://%3$s:%4$s#%1$s/%2$s', $host, $database, $username, $password, $port);
$isDevMode = true;
$dbParams = array(
'url' => $connectionString
, 'driver' => 'pdo_mysql'
, 'driverOptions' => array(
1002 => "SET NAMES utf8mb4"
)
);
$cacheDriver = null;
$config = \Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration(array(), $isDevMode, '.cache/', $cacheDriver, false);
if($cacheDriver){
$config->setMetadataCacheImpl($cacheDriver);
$config->setQueryCacheImpl($cacheDriver);
$config->setResultCacheImpl($cacheDriver);
}
$this->entityManager = \Doctrine\ORM\EntityManager::create($dbParams, $config);
return $this->entityManager;
}
/**
* #return \Doctrine\ORM\EntityManager
*/
public function getEntityManager(){
return $this->entityManager;
}
public function updateDatabaseSchema(){
$metaData = array();
$usedEntities = array(
'Test\Stackoverflow\User'
);
foreach($usedEntities as $entity){
$metaData[] = $this->entityManager->getClassMetadata($entity);
}
$tool = new \Doctrine\ORM\Tools\SchemaTool($this->entityManager);
$tool->updateSchema($metaData);
$this->generateProxies($metaData);
}
/**
* Generate all the proxy classes for orm in the correct directory.
* Proxy dir can be configured over application configuration
*
*
* #throws \Exception
*/
final public function generateProxies($metaData)
{
$em = $this->getEntityManager();
$destPath = $em->getConfiguration()->getProxyDir();
if (!is_dir($destPath)) {
mkdir($destPath, 0777, true);
}
$destPath = realpath($destPath);
if (!file_exists($destPath)) {
throw new \Exception("Proxy destination directory could not be created " . $em->getConfiguration()->getProxyDir());
}
if (!is_writable($destPath)) {
throw new \Exception(
sprintf("Proxies destination directory '<info>%s</info>' does not have write permissions.", $destPath)
);
}
if (count($metaData)) {
// Generating Proxies
$em->getProxyFactory()->generateProxyClasses($metaData, $destPath);
}
}
/**
* #var \Symfony\Component\Validator\Validator\ValidatorInterface
*/
protected $validator;
/**
* #return \Symfony\Component\Validator\Validator\ValidatorInterface
*/
final protected function getValidator(){
if($this->validator){
return $this->validator;
}
$this->validator = \Symfony\Component\Validator\Validation::createValidatorBuilder()
->enableAnnotationMapping()
->getValidator();
return $this->validator;
}
/**
* #param \Test\Stackoverflow\User $entity
* #return bool
*/
final public function testAnnotationWithoutLoading(\Test\Stackoverflow\User $entity){
try {
print "test to validate the entity without preloading the Assert classes\n<br>";
$this->getValidator()->validate($entity);
return true;
} catch(\Exception $e){
print "<strong>Does not work since the Asserts classes never get loaded: </strong> Exception-message: ".$e->getMessage()."\n<br>";
return false;
}
}
/**
* #param \Test\Stackoverflow\User $entity
* #return bool
*/
final public function testAnnotationWithLoading(\Test\Stackoverflow\User $entity){
// Here we force the autoloader to require the class
$notBlankValidator = new \Symfony\Component\Validator\Constraints\NotBlank();
try {
print "Loaded the validator manually, will test of it fails now\n<br>";
$this->getValidator()->validate($entity);
return true;
} catch(\Exception $e){
print "<strong>Was not working: </strong> Exception-message: ".$e->getMessage()."\n<br>";
print sprintf("<strong>Even when we autoload the class it is not working. Type of assert: %s</strong>\n<br>", get_class($notBlankValidator));
return false;
}
}
}
If you are using the Symfony Standard Edition, you must update your
autoload.php file by adding the following code [1]
How are these annotations loaded? From looking at the code you could
guess that the ORM Mapping, Assert Validation and the fully qualified
annotation can just be loaded using the defined PHP autoloaders. This
is not the case however: For error handling reasons every check for
class existence inside the AnnotationReader sets the second parameter
$autoload of class_exists($name, $autoload) to false. To work
flawlessly the AnnotationReader requires silent autoloaders which many
autoloaders are not. Silent autoloading is NOT part of the PSR-0
specification for autoloading. [2]
// at the top of the file
use Doctrine\Common\Annotations\AnnotationRegistry;
// at the end of the file
AnnotationRegistry::registerLoader(function($class) use ($loader) {
$loader->loadClass($class);
return class_exists($class, false);
});
[1] https://symfony.com/blog/symfony2-2-0-rc4-released
[2] https://www.doctrine-project.org/projects/doctrine-annotations/en/1.6/annotations.html
I'm trying to set up a Contact Form and all is going well. Set up my Controller with ->send(), all works fine (takes a bit of time). When I set it up to work with ->queue(), seems to work fine (no delay), job is set up, mail is sent when I dispatch. But this time my mail template does not include the data sent to the Mailer.
My Controller:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Mail\Contact;
use Illuminate\Support\Facades\Mail;
class PagesController extends Controller
{
public function sendContact(Request $request)
{
Mail::to('webform#email.com')
->queue(new Contact($request));
return redirect('/contact')->with('status', 'Message sent. Thanks!');
}
}
My Mailer (App\Mail\Contact):
class Contact extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct()
{
}
/**
* Build the message.
*
* #return $this
*/
public function build(Request $request)
{
$subject = 'Web Message from: ' . $request->name;
return $this->from('myemail#email.com')
->subject($subject)
->view('emails.contact-template')
->with([
'name' =>$request->name,
'email' => $request->email,
'message' => $request->message,
'date' => $request->date,
]);
}
}
The problem was that I needed to declare the variables as public. Below is the solution that eventually worked:
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Http\Request;
use Illuminate\Contracts\Queue\ShouldQueue;
class Contact extends Mailable
{
use Queueable, SerializesModels;
public $request;
public $name;
public $from;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct(Request $request)
{
$this->request = $request->all();
$this->name = $request->name;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
$subject = 'Webform messsage from: ' . $this->name;
$from = 'webform#mail.com';
return $this
->from( $from )
->subject($subject)
->view('emails.contact-template');
}
}