Is there a way to pass service/containers in child process? - php

I am testing spatie's async project. I created a task as such.
use Spatie\Async\Task;
class ServiceTask extends Task
{
protected $accountService;
protected $serviceFactory;
public function __construct(ServiceFactory $serviceFactory)
{
$this->serviceFactory = $serviceFactory;
}
public function configure()
{
$this->accountService = $this->serviceFactory->getAccountService();
}
public function run()
{
//accounting tasks
}
}
And for the pool:
$pool = Pool::create();
foreach ($transactions as $transaction) {
$pool->add(new ServiceTask($serviceFactory))
// handlers
;
}
$pool->wait();
When I run the above code, I simply get
Serialization of 'Closure' is not allowed
I know that we cannot simply serialize a closure, I tried the same code above with a simple plain Data Transfer Object, it worked fine. But when passing a service, or a container class from symfony I get above error. Is there a work around for this?

Short answer: no
Longer answer: Spatie Async serializes task before adding it to the pool, so you might need an alternative solution.
Why Async needs a serialized task
See relevant code to understand what's going on:
Pool::add > ParentRuntime::createProcess > ParentRuntime::encodeTask.
For more discussion, see this or this issue in spatie/async's issue list.
Alternative: Symfony Messenger
There are many alternatives to this problem. Since you're using Symfony, you might be interested in Symfony Messenger to send messages to a handler. Those handlers can use dependency injection:
class DefaultController extends AbstractController
{
public function index(MessageBusInterface $bus)
{
$bus->dispatch(new ServiceTask('Look! I created a message!'));
}
}
class ServiceTaskHandler implements MessageHandlerInterface
{
protected $accountService;
protected $serviceFactory;
public function __construct(ServiceFactory $serviceFactory)
{
$this->serviceFactory = $serviceFactory;
$this->accountService = $this->serviceFactory->getAccountService();
}
public function __invoke(ServiceTask $task)
{
$this->accountService->handle($task);
}
}
Be aware that a Task (ServiceTask in this example) should (like spatie/async's task) be serializable as well. So you might send an ID as a message, and look up that ID in your MessageHandler

Serialization of 'Closure' is not allowed is an error that is formed when passing a cued event as an argument. Likely the issue has to do with creating the object in a factory then passing it as part of the constructor.

Related

How to get Symfony 4 dependency injection working with two different use case scenarios?

We're trying to find the best way to implement dependency injection in a Symfony project with a quite specific problematic.
At user level, our application rely on an "Account" doctrine entity which is loaded with the help of the HTTP_HOST global against a domain property (multi-domain application). Going on the domain example.domain.tld will load the matching entity and settings.
At the devops level, we also need to do batch work with CLI scripts on many accounts at the same time.
The question we are facing is how to write services that will be compatible with both needs?
Let's illustrate this with a simplified example. For the user level we have this and everything works great:
Controller/FileController.php
public function new(Request $request, FileManager $fileManager): Response
{
...
$fileManager->addFile($file);
...
}
Service/FileManager.php
public function __construct(AccountFactory $account)
{
$this->account = $account;
}
Service/AccountFactory.php
public function __construct(RequestStack $requestStack, AccountRepository $accountRepository)
{
$this->requestStack = $requestStack;
$this->accountRepository = $accountRepository;
}
public function createAccount()
{
$httpHost = $this->requestStack->getCurrentRequest()->server->get('HTTP_HOST');
$account = $this->accountRepository->findOneBy(['domain' => $httpHost]);
if (!$account) {
throw $this->createNotFoundException(sprintf('No matching account for given host %s', $httpHost));
}
return $account;
}
Now if we wanted to write the following console command, it would fail because the FileManager is only accepting an AccountFactory and not the Account Entity.
$accounts = $accountRepository->findAll();
foreach ($accounts as $account) {
$fileManager = new FileManager($account);
$fileManager->addFile($file);
}
We could tweak in the AccountFactory but this would feel wrong...
In reality this is even worse because the Account dependency is deeper in services.
Does anyone have an idea how to make this properly ?
As a good practice, you should create an interface for the FileManager and set this FileManagerInterface as your dependency injection (instead of FileManager).
Then, you can have different classes that follow the same interface rules but just have a different constructor.
With this approach you can implement something like:
Service/FileManager.php
interface FileManagerInterface
{
// declare the methods that must be implemented
public function FileManagerFunctionA();
public function FileManagerFunctionB(ParamType $paramX):ReturnType;
}
FileManagerInterface.php
class FileManagerBase implements FileManagerInterface
{
// implement the methods defined on the interface
public function FileManagerFunctionA()
{
//... code
}
public function FileManagerFunctionB(ParamType $paramX):ReturnType
{
//... code
}
}
FileManagerForFactory.php
class FileManagerForFactory implements FileManagerInterface
{
// implement the specific constructor for this implementation
public function __construct(AccountFactory $account)
{
// your code here using the account factory object
}
// additional code that is needed for this implementation and that is not on the base class
}
FileManagerAnother.php
class FileManagerForFactory implements FileManagerInterface
{
// implement the specific constructor for this implementation
public function __construct(AccountInterface $account)
{
// your code here using the account object
}
// additional code that is needed for this implementation and that is not on the base class
}
Ans last but not least:
Controller/FileController.php
public function new(Request $request, FileManagerInterface $fileManager): Response
{
// ... code using the file manager interface
}
Another approach that also looks correct is, assuming that FileManager depends on an AccountInstance to work, changes could be made to your FileManager dependency to have the AccountInstance as a dependency instead of the Factory. Just Because in fact, the FileManager does not need the factory, it needs the result that the factory generates, so, automatically it is not FileManager's responsibility to carry the entire Factory.
With this approach you will only have to change your declarations like:
Service/FileManager.php
public function __construct(AccountInterface $account)
{
$this->account = $account;
}
Service/AccountFactory.php
public function createAccount():AccountInterface
{
// ... your code
}

Symfony 3 service depends on unknown quantity of another service. How to implement?

I'm fairly new to Symfony, but experienced with PHP. Suppose I have a service which needs an unknown quantity of another service. It doesn't make sense to inject it (how many would I inject). I can use the ContainerAwareInterface and ContainerAwareTrait but I've read that that's not a good way.
Slightly contrived example:
class ProcessBuilder {
private $allCommands = [];
public function build(array $config){
foreach ($config => $command){
$this->allCommands[] = $this->getContainer()->get('app.worker.command')->init($command);
}
}
}
At the point in which I get my ProcessBuilder service, I don't know how many items will be in the $config array passed into build(). Because of how the Command class (app.worker.command service) works, they cannot share a single instance.
How is the best way to do this? Or do I need to go down the ContainerAware* route?
I hope that makes sense and thanks for your help. Sorry if this has been asked before, but I've had a good Google and not come up with anything.
You are going the right direction. Now only right place is missing.
To collect services of certain type, we need to get 1 step ahead. To dependency injection container compilation (that's how services of EventSubscriber type or Voter type are collected by Symfony.)
You can register service with Extension, and manipulate them in any way with CompilerPass.
Example
Here is example how to collect all services of type A, and add them to service of type B with setter.
Your Case in Compiler Pass
If we convert your code to compiler pass, it would look like this:
ProcessBuilder.php
class ProcessBuilder
{
/**
* #var CommandInterface[]
*/
private $allCommands = [];
public function addCommand(CommandInterface $command)
{
$this->allCommands[] = $command
}
}
AddCommandsToProcessBuilderCompilerPass.php
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
final class AddCommandsToProcessBuilderCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $containerBuilder): void
{
# using Symfony 3.3+ class name, you can use string name as well
$processBuilderDefinition = $this->containerBuilder->getDefinition(ProcessBuilder::class);
foreach ($this->containerBuilder->getDefinitions() as $serviceName => $definition) {
if (is_subclass_of($definition->getClass(), CommandInterface::class)) {
$processBuilderDefinition->addMethodCall('addCommand', [new Reference($serviceName)]);
}
}
}
}
AppBundle.php
use Symfony\Component\HttpKernel\Bundle\Bundle;
final class AppBundle extends Bundle
{
public function build(ContainerBuilder $containerBuilder): void
{
$containerBuilder->addCompilerPass(new AddCommandsToProcessBuilderCompilerPass);
}
}
And add your bundle to AppKernel.php:
final class AppKernel extends Kernel
{
public function registerBundles()
{
bundles = [];
$bundles[] = new AppBundle;
}
}
That's complete process to do what you need in clean way in Symfony.

Laravel singleton not working across controller/ViewComposer

In Laravel, I have a class that I would like to make available to the service controller, make some changes to in the controller action, and then render out with a ViewComposer.
I have done this several times before without issue, but for some reason this time my usual approach is not working - clearly I'm doing something different, and I'm beginning to suspect I've fundamentally misunderstood an aspect of what I am doing.
I have a ServiceProvider with this register() method:
public function register()
{
$this->app->singleton(HelperTest::class, function ($app) {
$pb = new HelperTest();
$pb->test = "jokes on you batman";
return $pb;
});
}
Then in my controller I'm doing the following:
private $helper;
public function __construct(HelperTest $pb)
{
$this->helper = $pb;
$this->helper->test = "hahah";
}
And then I have a viewcomposer doing the following:
private $helper;
public function __construct(HelperTest $pb)
{
$this->helper = $pb;
}
public function compose(View $view)
{
$view->with('output', $this->helper->test);
}
When I call {{ $output }} in the blade view, I expect to see hahah, but instead I get jokes on you batman.
My debugging has shown that all three of these methods are definitely being called. It looks to me like the ViewComposer is for some reason instantiating its own, fresh instance of the class. What am I doing wrong?
Thanks!
Execute php artisan optimize on your console, this will generate an optimized class loader for your application, then check if you can find your class HelperTest registered in services.php inside boostrap/cache. Until HelperTest is not registered there, Laravel IoC can't resolve your class.

How to inject multiple classes that share the same interface in Laravel 4

Say I have an interface CrawlerInterface with implementation PageCrawler and FeedCrawler; if we happen to need both classes in a controller, how can that be achieved with constructor injection?
Previously we use a central ServiceProvider to register (i.e. App::bind) such classes, but in most cases we only have 1 implementation of an interface, so said problem hasn't occured to us yet.
PS: I also wonder if this problem suggests we should split the controller.
Updates:
Thanks for the comments and response, to explain, said interface has only one public method: crawl($uri), and both page/feed crawler implements it as given a resource identifier, return resource.
My follow up question:
Say we are in a calculator scenario where Addition, Subtraction and Multiplication share the same interface Operation, which has only 1 public method run, at some point we will still encounter this problem right? How do we handle situation like these in general with ServiceProvider?
If each crawler exists for a different reason, you can use arbitrary names for your instances, for example:
App::bind('crawler.allArticles', 'PageCrawler');
App::bind('crawler.latestArticles', 'FeedCrawler');
For the controller:
App::bind('CrawlerController', function($app) {
return new CrawlerController(
App::make('crawler.allArticles'),
App::make('crawler.latestArticles')
);
});
Your controller code would then use each crawler differently:
public function showLatestArticlesAction()
$latestArticles = $this->latestArticlesCrawler->crawl();
// ...
}
public function showAllArticlesAction()
$allArticles = $this->allArticlesCrawler->crawl();
// ...
}
If you just have a list of crawlers where each is used for the same thing, you probably want to do something like:
App::bind('crawlers', function($app) {
return [
App::make('PageCrawler'),
App::make('FeedCrawler'),
];
});
In your controller, you'll get a list of "crawlers" by configuring it like so:
App::bind('CrawlerController', function($app) {
return new CrawlerController(App::make('crawlers'));
});
Your controller code could be something like this:
public function showArticlesAction()
$allArticles = array();
foreach ($this->crawlers as $crawler) {
$allArticles = array_merge($allArticles, $this->crawler->crawl());
}
// ...
}
Ok lets assume you have a CrawlerController
class CrawlerController extends BaseController
{
protected $crawler1;
protected $crawler2;
public function __construct(CrawlerInterface $c1, CrawlerInterface $c2)
{
$this->crawler1 = $c1;
$this->crawler2 = $c2;
}
}
an interface
interface CrawlerInterface{}
and concrete implementations of that intefrace called PageCrawler and FeedCrawler
class PageCrawler implements CrawlerInterface{}
class FeedCrawler implements CrawlerInterface{}
You would inject the dependencies by writing a service locator like
App::bind('CrawlerController', function($app) {
$controller = new CrawlerController(
new PageCrawler,
new FeedCrawler
);
return $controller;
});
But as suggested by others you should rethink your logic, use it only if this kind
of architecture is unavoidable
I think that the interface won't help you in this case.
By doing:
App::bind('CrawlerInterface', '<implementation>');
You need to choose one:
App::bind('CrawlerInterface', 'PageCrawler');
or
App::bind('CrawlerInterface', 'FeedCrawler');
And then Laravel will inject it:
class CrawlerController {
public function __construct(CrawlerInterface $crawler)
{
}
}
To have both you have 2 options
-Have 2 different interfaces
-Inject the implementations directly:
class CrawlerController {
public function __construct(PageCrawler $pageCrawler, FeedCrawler $feedCrawler)
{
}
}
But I also think that, if you need something like this, you better rethink your logic.

Laravel 4 setting up model using the IoC container

I recently watched this video and wanted to change my Laravel controllers so that they had their dependencies managed with Laravel's IoC container. The video talks about creating an interface for a Model and then implementing that interface for the specific data source used.
My question is: when implementing the interface with a class that extends Eloquent and binding that class to the controller so that it is accessible from $this->model, should I also create interfaces and implementations for the Eloquent models which may be returned when calling methods such as $this->model->find($id)? Should there be different classes for the Model and the ModelRepository?
Put it another way: how do I do new Model when my model is in $this->model.
Generally, yes, people doing that pattern (the repository pattern) have an interface which have some methods defined that your app will use:
interface SomethingInterface {
public function find($id);
public function all();
public function paged($offset, $limit);
}
Then you create an implementation of this. If you're using Eloquent, then you can make an Eloquent implementation
use Illuminate\Database\Model;
class EloquentSomething {
protected $something;
public function __construct(Model $something)
{
$this->something = $something;
}
public function find($id)
{
return $this->something->find($id);
}
public function all() { ... }
public function paged($offset, $limit) { ... }
}
Then you make a service provider to put it all together, and add it into app/config/app.php.
use Something; // Eloquent Model
use Namespace\Path\To\EloquentSomething;
use Illuminate\Support\ServiceProvider;
class RepoServiceProvider extends ServiceProvider {
public function register()
{
$app = $this->app;
$app->bind('Namespace/Path/To/SomethingInterface', function()
{
return new EloquentSomething( new Something );
});
}
}
Finally, your controller can use that interface as a type hint:
use Namespace/Path/To/SomethingInterface;
class SomethingController extends BaseController {
protected $something;
public function __construct(SomethingInterface $something)
{
$this->something = $something;
}
public function home() { return $this->something->paged(0, 10); }
}
That should be it. Apologies on any errors, this isn't tested, but is something I do a lot.
Downsides:
More code :D
Upsides:
Able to switch out implementations (instead of EloquentSomething, can use ArraySomething, MongoSomething, whatever), without changing your controller code or any code that uses an implementation of your interface.
Testable - you can mock your Eloquent class and test the repository, or mock your constructor dependency and test your controller
Re-usable - you can App::make() to get the concrete EloquentSomething anywhere in your app and re-use the Something repository anywhere in your code
Repository is a good place to add additional logic, like a layer of cacheing, or even validation rules. Stock mucking about in your controllers.
Finally:, since I likely typed all that out and STILL DIDN'T ANSWER YOUR QUESTION (wtf?!), you can get a new instance of the model using $this->model. Here's an example for creating a new Something:
// Interface:
public function create(array $data);
// EloquentSomething:
public function create(array $data)
{
$something = this->something->newInstance();
// Continue on with creation logic
}
Key is this method, newInstance().
I've used $newModel = $this->model and it's worked for me.

Categories