I have a REST API built with Symfony2 and the FOSRestBundle. This all works fine, however in one service I combine data form another service in a different bundle - this seemed like it would be simple but it is not.
I am creating a new request object and adding in my parameters, from there I fire off the request to the other service, the service receives the request fine, however, when I try to use $this->get it gives me the good old Call to a member function get() on a non-object in ...
I know that I am missing the service container (I don't entirely understand why it's available when I call a hit the first bundle but not the second), that's all well and fine but how do I inject it or a component of it so I can use $this->get to hit my custom services defined in services.yml? (easy to pass them the service container using arguments:
container: "#service_container")
Setting this bundle up as a service won't work as FOSRestBundle does not call it as a service.
In Short: I want to be able to get data from bundle2 when inside bundle1 by doing
namespace MyVendor\Bundle1\Controller
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use MyVendor\Bundle2\Controller\Bundle2ClassName;
class Bundle1 {
//if i wanted to do this here it would work fine:
// $this->get('my.service.defined.in.service.yml');
$bundle2 = new Bundle2ClassName();
$returned_data = $bundle2->myFunction();
}
Then once inside myFunction in bundle2 if I try to call the exact same service function i get the dreaded get error. If I call bundle2 directly through the FOSRest route i obviously don't have that problem.
namespace MyVendor\Bundle2\Controller
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
class Bundle2 {
//this does not work
$this->get('my.service.defined.in.service.yml');
//do some stuff then return response
return($response);
}
I've read all the service container docs over and over again, so if you're going to link to them i'd appreciate it if you could point out the exact section where it explains how this stuff is handled. This has been the one problem i've never been able to fully understand since I started working with Symfony a few months back.
P.S. could someone with enough points add the FOSRestBundle as a tag?
Thanks!
First of all you should use $this->forward to forward request to another controller.
Second, the reason you don't have access to service container in second controller is probably because you're trying to manually initialize it - never do that unless you absolutely know what you're doing (specifically, you forgot to pass service container as controller dependency).
Third, just as an example on how things work - your original controller dependency on service container is handled by the same container and extends ContainerAware, on that controller initialization a setContainer() is called, which you most likely forgot to do when manually initializing second controller. So to get it working (which again I strongly recommend not doing), you should do this:
class Bundle1 {
//if i wanted to do this here it would work fine:
// $this->get('my.service.defined.in.service.yml');
$bundle2 = new Bundle2ClassName();
$bundle2->setContainer($this->container);
$returned_data = $bundle2->myFunction();
}
The reason you're getting the $this->get() on a non-object... error is because $this->get() in your controller is actually a shortcut to $this->container->get() (defined in Symfony\Bundle\FrameworkBundle\Controller\Controller)
Related
According to the docs, https://docs.akeneo.com/4.0/manipulate_pim_data/product/save.html, I should be able to call $saver = $this->get('pim_catalog.saver.product'); in my bundle's controller, like this:
<?php
namespace XXX\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
class ExportController extends Controller
{
public function exportProduct($id): Response
{
$saver = $this->get('pim_catalog.saver.product');
$saver->save($id);
return new Response(
'<html><body>foo</body></html>'
);
}
}
However I get this error:
[2020-11-05 13:44:58] request.CRITICAL: Uncaught PHP Exception Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: "The "pim_catalog.saver.product" service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead." at /var/www/html/pim/vendor/symfony/dependency-injection/Container.php line 275 {"exception":"[object] (Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException(code: 0): The "pim_catalog.saver.product" service or alias has been removed or inlined when the container was compiled. You should either make it public, or stop using the container directly and use dependency injection instead. at /var/www/html/pim/vendor/symfony/dependency-injection/Container.php:275)"} []
So I would want to override the declaration of this service by adding a services.YML file into my bundle's configuration directory (mybundle/Resources/config/services.YML) :
services:
pim_catalog.saver.product:
public: true
priority: 999
However it still doesn't work.
According to the documentation of Symfony 4, I should create an Extension class maybe. It should have the same name than Akénéo's one, but I don't find the latter.
What should I do?
You have some hints in the error message.
The "pim_catalog.saver.product" service or alias has been removed or
inlined when the container was compiled. You should either make it
public, or stop using the container directly and use dependency
injection instead
As a best practice, I would discourage making it public since you are not the service provider and there is a better and easier way to solve your issue. So you are left with the second option which is the way to go in my humble opinion.
I don't know specifically about Akeneo but I do know about Symfony and dependency injection. So I would suggest using the argument typehint in your controller méthod.
1. Check the autowiring class/interface to use for service 'pim_catalog.saver.product'
List all defined autowiring and look for the one mentioning 'pim_catalog.saver.product'
console debug:autowiring
OR just check for all autowired class/interface with akeneo in the name (this is likely)
console debug:autowiring akeneo
2. Update your controller code to add the type-hinted argument
Something in the lines of the following
<?php
namespace XXX\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
// I just picked that interface after looking it up in Akeneo documentation
// But there is another one that can be used if you want to save multiple
// products, so beware.
use Akeneo\Tool\Component\StorageUtils\Saver\SaverInterface;
class ExportController extends Controller
{
public function exportProduct($id, SaverInterface $saver): Response
{
$saver->save($id);
return new Response(
'<html><body>foo</body></html>'
);
}
}
The process is documented here https://symfony.com/doc/4.4/controller.html#fetching-services
3. (Hopefully) Enjoy a working code !
;-)
I am trying to reduce redundant code by refactoring template and controller code into reusable components, which I then use via the render(controller('AppBundle:Foo/Bar:baz')) construct inside my other templates.
Of course I would like to test these components. In the examples regarding functional testing, however, an actual route is required to make fake test requests. But my BarController here is purely internal and has no routes attached to it. How can I test this controller?
Creating dummy routes is not always possible, because some of the arguments are model objects that cannot be passed via URL. Am I approaching this the wrong way?
The service approach sounds nice, but I am simply doing this now:
self::$kernel->getContainer()->get('router')->getContext()->setParameter('_locale', 'en');
$controller = new MyController();
$controller->setContainer(self::$kernel->getContainer());
$response = $controller->myAction($arg1, $arg2, $argWhatever);
// assertions here
Seems to work just fine.
If the controllers are setup as services, then they can be easily tested much as any other class would be unit-tested. Even before Symfony 3.3 started to make them services by default, I had altered some of my own to allow them to be more easily tested like this.
I have a class which I load via dependency injection within Symfony 2.
It has a number of default private variables which I can override with setters.
Problem is, once one of those default values are set to something else within a page, there is no way to set it back automatically.
I don't want to call the class again using new as it removes the usefulness of the dependency injection.
So, does Symfony2 support Object LifeStyle, or do I basically need to write a public function which resets all of the defaults back to normal and call it each time it is required.
As an example, I use a class to get remote files. It has defaults for UserAgent, Accepted Language/Encoding, Method etc. But if I set the Method to POST, instead of the default GET for one call in a page, the next time I call it, it is still POST, where as I wish it to be the default GET once more, which it would be if I used new instead of via dependency injection.
Add scope: prototype to your service definition. This will ensure you get a new instance from the container each time you request your class. And of course the instance will have the default values.
http://symfony.com/doc/current/cookbook/service_container/scopes.html
I want to generate thumbnails after I upload the file. For image manipulations, I use Avalanche123's ImagineBundle.
I tried using the code in Entity:
$avalancheService = $this->get('imagine.cache.path.resolver');
$avalancheService->getBrowserPath($this->getUploadRootDir().'/'.$path.'/'.$extn[0].'.jpg', 'avatar');
But it doesn't help. What can I do?
On this line you try to get a service: $this->get('imagine.cache.path.resolver').
But a code example you're following is supposed to be executed in a controller. There is no get() method in an Entity, controller inherits it from Controller class that all controllers should extend. So calling $this->get() in an Entity is pointless.
This is by design. Entities in Symfony are supposed to be dumb and only represent data they have.
The right thing to do is to either do resize in your controller, or create a service, inject imagine.cache.path.resolver into it and call it from your controllers.
Check the variables using dump(var) in twig. Perhaps $extn[0] doesn't have the correct file or isn't set. Try poutputting the full getBrowserPath string to make sure it's accurate.
I am creating a custom route by subclassing RouteBase. I have a dependency in there that I'd like to wire up with IoC. The method GetRouteData just takes HttpContext, but I want to add in my unit of work as well....somehow.
I am using StructureMap, but info on how you would do this with any IoC framework would be helpful.
Well, here is our solution. Many little details may be omitted but overall idea is here. This answer may be a kind of offtop to original question but it describes the general solution to the problem.
I'll try to explain the part that is responsible for plain custom HTML-pages that are created by users at runtime and therefore can't have their own Controller/Action. So the routes should be either somehow built at runtime or be "catch-all" with custom IRouteConstraint.
First of all, lets state some facts and requirements.
We have some data and some metadata about our pages stored in DB;
We don't want to generate a (hypothetically) whole million of routes for all of existing pages beforehand (i.e. on Application startup) because something can change during application and we don't want to tackle with pushing the changes to global RouteCollection;
So we do it this way:
1. PageController
Yes, special controller that is responsible for all our content pages. And there is the only action that is Display(int id) (actually we have a special ViewModel as param but I used an int id for simplicity.
The page with all its data is resolved by ID inside that Display() method. The method itself returns either ViewResult (strongly typed after PageViewModel) or NotFoundResult in case when page is not found.
2. Custom IRouteConstraint
We have to somewhere define if the URL user actually requested refers to one of our custom pages. For this we have a special IsPageConstraint that implements IRouteConstraint interface. In the Match() method of our constraint we just call our PageRepository to check whether there is a page that match our requested URL. We have our PageRepository injected by StructureMap. If we find the page then we add that "id" parameter (with the value) to the RouteData dictionary and it is automatically bound to PageController.Display(int id) by DefaultModelBinder.
But we need a RouteData parameter to check. Where we get that? Here comes...
3. Route mapping with "catch-all" parameter
Important note: this route is defined in the very end of route mappings list because it is very general, not specific. We check all our explicitly defined routes first and then check for a Page (that is easily changeable if needed).
We simply map our route like this:
routes.MapRoute("ContentPages",
"{*pagePath}",
new { controller = "Page", action = "Display" }
new { pagePath = new DependencyRouteConstraint<IsPageConstraint>() });
Stop! What is that DependencyRouteConstraint thing appeared in mapping? Well, thats what does the trick.
4. DependencyRouteConstraint<TConstraint> class
This is just another generic implementation of IRouteConstraint which takes the "real" IRouteConstraint (IsPageConstraint) and resolves it (the given TConstraint) only when Match() method called. It uses dependency injection so our IsPageConstraint instance has all actual dependencies injected!
Our DependencyRouteConstraint then just calls the dependentConstraint.Match() providing all the parameters thus just delegating actual "matching" to the "real" IRouteConstraint.
Note: this class actually has the dependency on ServiceLocator.
Summary
That way we have:
Our Route clear and clean;
The only class that has a dependency on Service Locator is DependencyRouteConstraint;
Any custom IRouteConstraint uses dependency injection whenever needed;
???
PROFIT!
Hope this helps.
So, the problem is:
Route must be defined beforehand, during Application startup
Route's responsibility is to map the incoming URL pattern to the right Controller/Action to perform some task on request. And visa versa - to generate links using that mapping data. Period. Everything else is "Single Responsibility Principle" violation which actually led to your problem.
But UoW dependencies (like NHibernate ISession, or EF ObjectContext) must be resolved at runtime.
And that is why I don't see the children of RouteBase class as a good place for some DB work dependency. It makes everything closely coupled and non-scalable. It is actually impossible to perform Dependency Injection.
From now (I guess there is some kind of already working system) you actually have just one more or less viable option that is:
To use Service Locator pattern: resolve your UoW instance right inside the GetRouteData method (use CommonServiceLocator backed by StructureMap IContainer). That is simple but not really nice thing because this way you get the dependency on static Service Locator itself in your Route.
With CSL you have to just call inside GetRouteData:
var uow = ServiceLocator.Current.GetService<IUnitOfWork>();
or with just StructureMap (without CSL facade):
var uow = ObjectFactory.GetInstance<IUnitOfWork>();
and you're done. Quick and dirty. And the keyword is "dirty" actually :)
Sure, there is much more flexible solution but it needs a few architectural changes. If you provide more details on exactly what data you get in your routes I can try to explain how we solved our Pages routing problem (using DI and custom IRouteConstraint).