Symfony and Api-Platform: Use Doctrine and other Services outside controllers - symfony

Hello Stackoverflow
We have a problem with Symfony and API-PLATFORM. We tried a lot of things, but couldn't find any solution. We had difficulties using the answers already given on stackoverflow and the git issues they have that are related to this question. This is why this is not a duplicate in any form, since those sadly didn't help us.
The problem is as follows. We have the standard structure in our src folder of api-platform.
- src
-- Annotation
-- Controller
-- Doctrine
-- Emails
-- Entity
-- EventListener
-- EventSubscriber
-- Filters
-- Repository
-- Security
-- Utils
So here is were our problem starts. We have a few classes which are just some utilities for easy use. Those are in the utils folder. This question is about a class in the Utils folder named Phone. (namespace App\Utils\Phone). The thing here is, that we need to use the EntityManager in this Utils class, because we need to store some of the data (Temporarily). We'll do this with the following code (Which works in controllers)
$em = $this->getDoctrine()->getManager();
$em->persist($smsStore);
$em->flush();
Now the error hits: Doctrine doesn't even exist here. It cannot be found. The question here is: How can we use the Doctrine EntityManager outside of the Controllers? We tried extending by using the default AbstractController, but this cannot be correct and doesn't work.
The same thing happens when we want to use our security class to get the current user. ($this->security->getUser()). Even when we import it like with a use. use Symfony\Component\Security\Core\Security;.
We hope we were clear with this question. We're a bit desperate because we're still looking for a solution after a few days. We use PHP version 7.1.3 and API-PLATFORM version 1.1 (composer.json)
EDIT 1: The does not exist is about an undefined method (getDoctrine). We tried a few things to implement this. For example using the AbstractController to extend the class. Another solution that worked is really pass the DoctrineManager with the function. This is however not a good practice.
EDIT 2: The AuthController snippet as requested. It shows how we use the Phone Utils. Could it be that I need to implement the EntityInterface at this function surrounding the following code?
$phone = new Phone();
$phone->overwriteUser($this->getUser());
$phone->newRecipient("4917640733908");
$phone->setBody("Hello, this is a test.");

Entity manager could be handle with the dependency injection(DI) pattern. This pattern is fully integrated in Symfony. This is the fun and magic aspect of Symfony.
All classes implementing an interface are available with dependency injections. With Symfony4+, the autoconfigure is activated. It seems to be complex, but for the final developer, it's pretty easy. Here is a simple example to do it with your Phone classes which seems to be a model where you have implemented you business logic.
class Phone
{
private $entityManager;
//The entity manager will be provided to your constructor with the dependency injection.
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function save($smsStore)
{
//you own logic go here
//here is the magic part, the entityManager is available, connected, ready to use.
$this->entityManager->persist($smsStore);
$em->flush();
}
}
When you'll call the Utils classes method, the DI will construct it automatically and you will be able to use its property, the entity manager in this case.
If you want to use you class in a controller, you can do it by adding the phone class in its constructor:
class AuthController extends AbstractController
{
public function __construct(Phone $phoneUtils)
{
$this->phoneUtils = $phoneUtils;
}
public function someAction()
{
//your own logic here
//here is the magic part, the phone Utils is ready, it imports the entitymanager
//when you want to save your smsStore, simply call it
$this->phoneUtils->save($smsStore);
}
}
You are using Api-Platform, no problem! You can reproduce this design pattern in other classes like your DataPersisters, or in your EventListeners. If you look at this example in the ApiPlateform documentation, you can see how author get the Swiftmailer manager in his event listener. Simply replace the swiftmailer by EntityManagerInterface or the Phone utils classes.
(In a controller, it exists a simpliest way to retrieve the phone utils class)
class AuthController extends AbstractController
{
//Do That: ADD Phone $phoneUtils as an argument in the method
public function someAction(Phone $phoneUtils)
{
//your own logic here
//Do not do that: $phoneUtils = new Phone();
//Magic! when you want to save your smsStore, simply call it
$phoneUtils->save($smsStore);
}
}
If you want to get an autowired class (like the security layer), you could do it. To find which classes could be autowired, there are some tips:
symfony console debug:autowiring
symfony console debug:autowiring Security
symfony console debug:autowiring AuthorizationChecker
It will return all the classes corresponding to your search.
(If you do not use the symfony executable, you could replace it by php bin/console debug:autowiring)

Related

getUser method from Symfony Security can't get my User Repository

My problem is the $this->getUser(); method (called from Controller) from Symfony Security can't get my User Repository.
The "App\Model\ODM\Repository\UserRepository" document repository implements "Doctrine\Bundle\MongoDBBundle\Repository\ServiceDocumentRepositoryInterface", but its service could not be found. Make sure the service exists and is tagged with "doctrine_mongodb.odm.repository_service".
I have a CompilerPass that get my Repositories and add them to Container Definitions depending on some env value.
I add the Repo with "generic" Interfaces (valid for both ODM and ORM) as keys AND with the Repo FQCN as keys, so they should be accessible via both the generic Interface and the true Repo name, but I'm working only with ODM for now.
Also, I "tag" my repo with doctrine_mongodb argument, to extends the ServiceDocumentRepository baseClass.
I don't include the compilerPass because the class is quite "heavy", but if needed I'll include it.
Here is my UserRepo
class UserRepository extends BaseRepository implements RepositoryInterface, UserRepositoryInterface
{
/**
* UserRepository constructor.
*
* #param ManagerRegistry $managerRegistry
*/
public function __construct(ManagerRegistry $managerRegistry)
{
parent::__construct($managerRegistry, User::class);
}
...
My BaseRepo (just a few methods in here, just the extends is interesting)
abstract class BaseRepository extends ServiceDocumentRepository implements RepositoryInterface
{
...
It's working great "in-app", I could create a User on a registration page, but using $guardHandler->authenticateUserAndHandleSuccess() method after register, i'm redirected to my homepage where the $this->getUser(); is, and then I have the error message posted above.
After some research, it seems the problem is in Doctrine\Bundle\MongoDBBundle\Repository\ContainerRepositoryFactory
The container of this class doesn't seem to have any of my Repo definition.
I surely missed something but I don't know where.
I remember making something similar few years ago but extending DocumentRepository and not ServiceDocumentRepository.
If you need any other information, let me know.
I feel really dumb about this question now that I found the solution ...
It was LITTERALY in the error message, I just had to add doctrine_mongodb.odm.repository_service to my Repo's Definition, in my Compiler Pass
...
$definition->addArgument(new Reference('doctrine_mongodb'));
$definition->addTag('doctrine_mongodb.odm.repository_service');
...
Thanks to the people who looked at my problem and sorry for wasting your time.
I Hope this answer could help others peoples that register Repo in a CompilerPass
(I will set this answer as the solution asap, in 2 days)
Did you enable autowire in your services.yml?
services:
_defaults:
autowire: true
autoconfigure: true
App\Repository\:
resource: '../src/Repository/*'
Ill put my solution in case someone else has this problem and the one above doesn't work. For me what worked was to put the whole path to the repository when it was declared in the Document class. Like so:
// /src/Document/User
/**
* #MongoDB\Document(collection="users", db="database", repositoryClass=\App\Repository\UserRepository::class)
* #MongoDBUnique(fields="email")
*/
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
I didn'nt have to add anything in my Compiler Pass. I added the full path to the repository class in its declaration and it instantly worked. Hope that it helps.

Differences between different methods of Symfony service collection

For those of you that are familiar with the building of the Symfony container, do you know what is the differences (if any) between
Tagged service Collector using a Compiler pass
Tagged service Collector using the supported shortcut
Service Locator especially, one that collects services by tags
Specifically, I am wondering about whether these methods differ on making these collected services available sooner or later in the container build process. Also I am wondering about the ‘laziness’ of any of them.
It can certainly be confusing when trying to understand the differences. Keep in mind that the latter two approaches are fairly new. The documentation has not quite caught up. You might actually consider making a new project and doing some experimenting.
Approach 1 is basically an "old school" style. You have:
class MyCollector {
private $handlers = [];
public function addHandler(MyHandler $hamdler) {
$handlers[] = $handler;
# compiler pass
$myCollectorDefinition->addMethodCall('addHandler', [new Reference($handlerServiceId)]);
So basically the container will instantiate MyCollector then explicitly call addHandler for each handler service. In doing so, the handler services will be instantiated unless you do some proxy stuff. So no lazy creation.
The second approach provides a somewhat similar capability but uses an iterable object instead of a plain php array:
class MyCollection {
public function __construct(iterable $handlers)
# services.yaml
App\MyCollection:
arguments:
- !tagged_iterator my.handler
One nice thing about this approach is that the iterable actually ends up connecting to the container via closures and will only instantiate individual handlers when they are actually accessed. So lazy handler creation. Also, there are some variations on how you can specify the key.
I might point out that typically you auto-tag your individual handlers with:
# services.yaml
services:
_instanceof:
App\MyHandlerInterface:
tags: ['my.handler']
So no compiler pass needed.
The third approach is basically the same as the second except that handler services can be accessed individually by an index. This is useful when you need one out of all the possible services. And of course the service selected is only created when you ask for it.
class MyCollection {
public function __construct(ServiceLocator $locator) {
$this->locator = $locator;
}
public function doSomething($handlerKey) {
/** #var MyHandlerInterface $handler */
$handler = $serviceLocator->get($handlerKey);
# services.yaml
App\MyCollection:
arguments: [!tagged_locator { tag: 'app.handler', index_by: 'key' }]
I should point out that in all these cases, the code does not actually know the class of your handler service. Hence the var comment to keep the IDE happy.
There is another approach which I like in which you make your own ServiceLocator and then specify the type of object being located. No need for a var comment. Something like:
class MyHandlerLocator extends ServiceLocator
{
public function get($id) : MyHandlerInterface
{
return parent::get($id);
}
}
The only way I have been able to get this approach to work is a compiler pass. I won't post the code here as it is somewhat outside the scope of the question. But in exchange for a few lines of pass code you get a nice clean custom locator which can also pick up handlers from other bundles.

Doctrine EntityManagerDecorator

I've created custom decorator for EntityManager and now when I'm doing doctrine->getManager(), then I can get my custom manager class, but inside repository class I still have native EntityManager how can I fix this. Or maybe there is another way to set something inside repository classes from container?
Decorator calls getRepository on $wrapped(EntityManager) and then $wrapped pass $this inside RepositoryFactory $this == $wrapped == EntityManager
My solution is:
public function getRepository($className)
{
$repository = parent::getRepository($className);
if ($repository instanceof MyAbstractRepository) {
$repository->setDependency();
}
return $repository;
}
There are a couple of approaches:
Copy the static EntityManager::createRepository code to your entity manager class and adjust it accordingly. This is fragile since any change to the EntityManager code might break your code. You have to keep track of doctrine updates. However, it can be made to work.
A second approach is to define your repositories as services. You could then inject your entity manager in the repository. Bit of a hack but it avoids cloning the createRepository code.
The third approach is the recommended approach. Don't decorate the entity manager. Think carefully about what you are trying to do. In most cases, Doctrine events or a custom base repository class can handle your needs. And it saves you from fooling around with the internals.
One option would be to override the entity manager service classes or parameters via a compiler pass.

Symfony2 using a "global function" inside a form without overkill

I know I can do a lot of stuff...but I'd like to avoid overkill.
I have a class with general util methods. I want to be able to call one of those methods from inside a formType.
What I did is the following:
Inside the formType I added
use AppBundle\Util\GeneralUtil;
class HRMgmtFormType extends AbstractType
{
public function __construct(GeneralUtil $util)
{
$this->util = $util;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$timeCommitment = $this->util->generatePercentVectorByStep(1);
...all the rest of the stuff
And when I call it from the controller I have:
$form = $this->createForm(new HRMgmtFormType(new GeneralUtil()), array(...all the stuff I need here...);
It works.
Is this ok/elegant?
I basically avoided declaring my general util class as a service, and most important declaring my form as a service (and injecting the generalUtil service inside).
Thank you!
SN
As far as you have the class "GeneralUtil" such simple as you have it now: without any dependencies and any configuration parameters, your solution is fair. But there are some more aspects that you should think about:
Will be "GeneralUtil" reusable at other places?
Will you need to make unit tests? if so, do you plan to mock the util if you plan to get "pure" unit test and make dependency injection(DI) with mock?
Do you plan extend the util's functionality? will it get some dependencies or configuration in future?
If yes, then it is better to get the benefit from Symfony SOA (Service Oriented Architecture) approach and refactor your code to SOA and DI, that will allow you follow another important patter as DRY (Don't repeat yourself) http://en.wikipedia.org/wiki/Don%27t_repeat_yourself
Update to your commet:
So you can see from http://symfony.com/doc/current/book/forms.html#defining-your-forms-as-services
Defining your form type as a service is a good practice and makes it
really easy to use in your application.
but as I already explained about the utils class, similar logic is about forms. So if you plan to reuse your form, then you can do it as service, but if you will have several different forms that will use your utils class, then it's better leave the form as class, but make a service for the utils. In addition you can do it if you see possibility to overwrite your form some other i.e 3rd party forms.
So benefits list from form as service:
in case of multiply usage, it initialized only once
easy to overwrite by other form
globally configured from parameters ans DI other services

Injecting the Doctrine Entity Manager in services - Bad practice?

Using https://insight.sensiolabs.com to scan / check my code, I get the following warning:
The Doctrine Entity Manager should not be passed as an argument.
Why is it such a bad practice to inject the Entity Manager in a service? What is a solution?
With respect to the comment that repositories cannot persist entities.
class MyRepository extends EntityRepository
{
public function persist($entity) { return $this->_em->persist($entity); }
public function flush () { return $this->_em->flush (); }
I like to make my repositories follow more or less a "standard" repository interface. So I do:
interface NyRepositoryInterface
[
function save($entity);
function commit();
}
class MyRepository extends EntityRepository implements MyRepositoryInterface
{
public function save ($entity) { return $this->_em->persist($entity); }
public function commit() { return $this->_em->flush (); }
This allows me to define and inject non-doctrine repositories.
You might object to having to add these helper functions to every repository. But I find that a bit of copy/paste is worth it. Traits might help here as well.
The idea is move away from the whole concept of an entity manager.
I am working on a quite a large project currently and have recently started following the approach with repositories that can mutate data. I don't really understand the motivation behind having to inject EntityManager as a dependency which is as bad as injecting ServiceManager to any class. It is just a bad design that people try to justify. Such operations like persist, remove and flush can be abstracted into sth like AbstractMutableRepository that every other repository can inherit from. So far it has been doing quite well and makes code more readable and easier to unit test!
Show me at least one example of a service that has EM injected and unit test for it looked correctly? To be able to unit test sth that has EM injected is more about testing implementation than anything else. What happens is then you end up having so many mocks set up for it, you cannot really call it a decent unit test! It is a code coverage hitter, nothing more!

Resources