I'm attempting to extend akeneo/MeasureBundle via the method described here. I've added a couple of methods to the MeasureManager and added a compiler pass to substitute my version of the MeasureManager for the original. This all works.
I've also created some custom form types: UnitFamiliesType, MeasurementType, UnitType. I want these to be services so I put them in my services.yml file which looks like this:
services:
acrdMeas.form.measurement.type:
class: ACRD\MeasureBundle\Form\Type\MeasurementType
scope: prototype
arguments: [ "#akeneo_measure.manager" ]
tags:
- { name: form.type, alias: acrdMeas_measurement }
acrdMeas.form.unitfamilies.type:
class: ACRD\MeasureBundle\Form\Type\UnitFamilyType
arguments: ["#akeneo_measure.manager"]
tags:
- { name: form.type, alias: acrdMeas_unitfamilies }
acrdMeas.form.units.type:
class: ACRD\MeasureBundle\Form\Type\UnitFamilyType
arguments: ["#akeneo_measure.manager"]
tags:
- { name: form.type, alias: acrdMeas_units }
Unfortunately, these formtypes do not show up as services when I run app/console container:debug. Attempting to create a form with any of them results in a "Could not load type ..." error. I did test the form types by instantiating them directly via new and that worked fine.
Why is my extended bundle not processing the services.yml file?
How do I add extra services to my extended bundle?
It turns out that extending the BundleExtension file is what works:
namespace ACRD\MeasureBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Loader;
use Akeneo\Bundle\MeasureBundle\DependencyInjection\AkeneoMeasureExtension as Extension;
class ACRDMeasureExtension extends Extension {
public function load(array $configs, ContainerBuilder $container){
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
}
I'm not really clear why the child load() method doesn't clobber parent::load(), but it doesn't. But hopefully this helps someone.
Related
In my App I have a helper class App\Command\GenerateFixturesCommand that provides a command named my-nice-project:generate-fixtures.
This command consumes a service of my own project named App\Services\CatalogFixtureGenerator that generates 1000 random PDF documents for testing while developing the app.
To do so, this service uses the joshtronic\LoremIpsum class which is required in composer only in dev. LoremIpsum is a third-party library. I require it under composer's require-dev.
So the injection is:
I run my GenerateFixturesCommand.
Before that, the system transparently locates my CatalogFixtureGenerator and to inject it into the command.
Before that, the system transparently locates the LoremIpsum third party service to inject it into my fixture generator service.
All is autowired.
When I deploy to prod and do composer install --no-dev --optimize-autoloader of course the LoremIpsum class is not installed.
But when I clear the cache with APP_ENV=prod php bin/console cache:clear the framework finds the command and cannot inject the autowired dependencies.
[WARNING] Some commands could not be registered:
In CatalogsFixtureGenerator.php line 26:
Class 'joshtronic\LoremIpsum' not found
This my-nice-project:generate-fixtures command is never going to be used in the production server.
Question
How can I "disable" the command in prod?
I mean: How can I tell the framework that the class GenerateFixturesCommand should not be loaded nor its autowired dependencies, and neither of them should be autowired in prod?
Use the isEnabled() method in Command.
For example
public function isEnabled(): bool
{
// disable on prod
if ($this->appKernel->getEnvironment() === 'prod') {
return false;
}
return true;
}
In my last project, I need some commands to work only in dev environment. You use getenv function to achieve this:
# src/Command/SomeCommand.php
...
public function __construct()
{
parent::__construct();
if (getenv("APP_ENV") !== "dev") {
exit('This command should work only "dev" environment.');
}
}
This will do the trick.
Code fun :)
The solution #gusDeCooL suggests doesn't work with lazy-loaded commands (at least not for me).
I ended up implementing the isEnabled() method anyway, but then I added a guard in execute():
<?php
namespace App\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'command:name',
description: 'Some description',
)]
class CommandName extends Command
{
public function isEnabled(): bool
{
return 'dev' === getenv('APP_ENV');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
if (!$this->isEnabled()) {
$io->error('This command is only available in `dev` environment.');
exit(1);
}
// the rest
}
}
I am working on a third party bundle which is in the vendor/ directory.
I have an Entity class which looks like this:
/**
* #ORM\Entity(repositoryClass="Acme\DemoBundle\Repository\ArticleRepository")
* #ORM\Table(name="acme_demo_article")
*/
class Article
And a Repository class like this:
class ArticleRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry)
{
parent::__construct($registry, Article::class);
}
}
This generates the following error:
The "Acme\DemoBundle\Repository\ArticleRepository" entity repository
implements
"Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface",
but its service could not be found. Make sure the service exists and
is tagged with "doctrine.repository_service".
If i remove the repositoryClass from the entity definition, I dont have the error anymore and i can use doctrine as such from my controller:
this->getDoctrine()->getRepository(Article::class)->findBy([], null, $limit, ($page - 1) * $limit);
I tried adding the repository as a service in the bundle service definition but it does not change anything:
vendor/Acme/demo-bundle/Resources/config/services.yaml
services:
Acme\DemoBundle\Repository\:
resource: '../../Repository/ArticleRepository.php'
autoconfigure: true
tags: ['doctrine.repository_service']
bin/console debug:autowire or debug:container wont show the service.
I also tried adding the extension:
namespace Acme\BlogBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
class AcmeBlogExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__.'/../Resources/config')
);
$loader->load('services.xml');
}
}
Did not work either. I dont have the impression that the extension is being called. I tried adding a constructor to it and dump, die in the constructor, but there are no results of the dump.
So my question is how do i define my repositories as a service from the vendor directory ?
The source code is overhere: https://github.com/khalid-s/sf4-bundle-test
After much struggling, i succedded in my task. I dont think that's it should be done like this, but if this can help someone...
I added in my DependencyInjection folder of the bundle:
class AcmeBlogExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader(
$container,
new FileLocator(__DIR__.'/../Resources/config')
);
$loader->load('services.yaml');
}
}
I created a compiler (this is the part which i struggled to figure out) to register my service
class RepositoryCompiler implements CompilerPassInterface
{
/**
* #inheritdoc
*/
public function process(ContainerBuilder $container)
{
$container->register('acme_blog.repository', ArticleRepository::class);
}
}
I added in my Bundle class:
class AcmeBlogBundle extends Bundle
{
/** #info this function normally is useless */
public function getContainerExtension()
{
// This is only useful if the naming convention is not used
return new AcmeBlogExtension();
}
/**
* #inheritDoc
*/
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new RepositoryCompiler());
parent::build($container);
}
}
And finally the service itself:
services:
Acme\BlogBundle\Repository\:
resource: '../../Repository/*Repository.php'
autoconfigure: true
autowire: true
tags: ['doctrine.repository_service']
The autoconfigure and autowire are useless since they are not taken into consideration when i debug:container which looks like this:
php bin/console debug:container acme
Information for Service "acme_blog.article.repository"
=======================================================
---------------- -----------------------------------------------
Option Value
---------------- -----------------------------------------------
Service ID acme_blog.article.repository
Class Acme\BlogBundle\Repository\ArticleRepository
Tags doctrine.repository_service
Public yes
Synthetic no
Lazy no
Shared yes
Abstract no
Autowired no
Autoconfigured no
---------------- -----------------------------------------------
One very important note which made me loose a lot of time:
Do clear your cache after every change to your services. Even in dev
mode they are not reloaded after every refresh
I have a simple problem and documentation is not helping me resolve it.
I have created a Grails v3.3.3 demo project - and created a simple domain class called JsonApiBook, with 'name' attribute like this
package ttrestapi
import grails.rest.*
#Resource (uri='/jsonApiBook', formats=['json','xml'])
class JsonApiBook {
static constraints = {
}
String name
}
and marked up the URI as the documentation says the JSON API rendering only works with domain classes (and not a controller class).
In my bootstrap I have saved a instance of book to the tables - and can view that generally.
In my views directory I have a created jsonApiBook folder and created two gson files.
A '_jsonApIBook' template like this
import ttrestapi.JsonApiBook
model {
JsonApiBook book
}
json jsonapi.render(book)
which invokes the jsonapi helper object to render the instance.
I have in the same directory created an index.json like this:
import ttrestapi.Book
model {
List<Book> bookList
}
// We can use template namespace
// method with a Collection.
json tmpl.book(bookList)
When I run the app and use postman or browser to render then I get a result but its Json api compliant (I think it's ignored the template).
So localhost:8080/jsonApiBook just returns (looks default layout):
[
{
"id": 1,
"name": "json api book3"
}
]
and localhost:8080/jsonApiBook/1 just returns 'null' which can't be right.
How should I be setting up the json views for rendering JSON API compliant output? As this doesn't appear to work correctly.
build.gradle
buildscript {
....
dependencies {
........
classpath "org.grails.plugins:views-gradle:1.2.7"
}
}
--
apply plugin: "org.grails.grails-web"
apply plugin: "org.grails.plugins.views-json"
dependencies {
. . .
compile "org.grails.plugins:views-json:1.2.7"
. . .
}
Domain JsonApiBook.groovy
import grails.rest.Resource
#Resource (uri='/jsonApiBook', formats=['json','xml'])
class JsonApiBook {
String name
static constraints = {
}
}
Bootstrap.groovy
class BootStrap {
def init = { servletContext ->
new JsonApiBook(name: 'first').save(flush:true)
new JsonApiBook(name: 'second').save(flush:true)
new JsonApiBook(name: 'third').save(flush:true)
new JsonApiBook(name: 'fourth').save(flush:true)
new JsonApiBook(name: 'fifth').save(flush:true)
}
def destroy = {
}
}
Created folder under view called jsonApiBook
Created template named _jsonApiBook.gson in jsonApiBook folder
model {
JsonApiBook jsonApiBook
}
json {
name jsonApiBook.name
}
created show.gson under same folder
model {
JsonApiBook jsonApiBook
}
json g.render(template:"jsonApiBook", model:[jsonApiBook:jsonApiBook])
When i run http://localhost:8080/jsonApiBook i get bellow:
When i run http://localhost:8080/jsonApiBook/1 i get bellow:
Note: I used grails 3.3.3 with h2 memory DB
Reference
Hope this helps you
ok - got to similar place today on the train. Essentially the convention over configuration is core to whats happening here.
First the #Resource annotation generates a default RestfulController for you. In this approach the default base template _resourceClassName.gson expects the model variable to have the same name as the resource type so my original example instead of 'book'
import ttrestapi.JsonApiBook
model {
JsonApiBook book
}
json jsonapi.render(book)
it should really read as (following convention)
import ttrestapi.JsonApiBook
// variable should be same name as the Class name starting with lowercase
// as default (it can be different but the caller has to change how the
// the template parameter is invoked
model {
JsonApiBook jsonApiBook
}
json jsonapi.render(jsonApiBook)
Then the index.gson should have read as modified below
import ttrestapi.JsonBookApi
//note although not obvious in the written docs which use the show command, the
// default expected model variable is <resourceClass>List
model {
List<JsonBookApi> jsonBookApiList
}
// We can use template namespace
// method with a Collection.
json tmpl.jsonBookApi (jsonBookApiList )
If you want to use another variable name then in the base template you'd have to declare that name as map when calling the base template, from the index.gson . e.g. say the variable name in the base template was
model {
JsonBookApi myBook...
then when calling this template from my index.gson you would put something like this
...
model {
List<JsonBookApi> jsonBookApiList
}
json tmpl.jsonBookApi ("myBook", jsonBookApiList )
this invokes the correct template _jsonBookApi, but takes the model variable default in the index.gson and forces it to bind the value of jsonBookApiList to the myBook variable in the base template (_jsonBookApi.gson).
With the default generation of a controller, using #Resource annotation, the model variable will always be 'resourceClassName'List
I think the only way to change that is not to use the #Resource annotation on your domain class, but to use the URL mappings configuration to map your uri to a controller, and then you have to create a controller yourself by hand and ensure you extend from RestfulController. doing this you can override the default model variable name by implementing an overidden 'index()' method and ensuring you explicitly name the model variable you want, and ensure that the index.gson model variable is exactly the same as that set in your controller.
however the key point was I was not following the core convention defaults so the code as originally built couldn't work and returned null.
when you start out the documentation isn't absolutely clear what bits are part of the convention, and in the examples (which use show.gson) don't tell you what the model variable default name will be for the index.gson (add List to end) so its quite easy to get lost
How would I inject an addon service into other 'places'?
For example, if I install an addon that injects into controllers & components with the code below:
export default {
name: 'notification-messages-service',
initialize() {
let application = arguments[1] || arguments[0];
application.register('notification-messages:service', NotificationMessagesService);
['controller', 'component'].forEach(injectionTarget => {
application.inject(injectionTarget, 'notifications', 'notification-messages:service');
});
}};
How would I then inject the same service (the same singleton) into services & routes - my requirement is actually inject into a single service, services:messages?
I don't believe I can use
notifications: Ember.inject.service();
because in the addon the service is written as:
export default Ember.ArrayProxy.extend({...});
I can change the addon, of course, but my changes would be gone once the addon is updated.
Thanks for looking, N
notifications: Ember.inject.service('notification-messages');
should work
param for service is optional if name of service the same as property name, but it's bette always use it
P.S. code above for normal services
in you case code
['controller', 'component'].forEach(injectionTarget => {
application.inject(injectionTarget, 'notifications', 'notification-messages:service');
});
means that controllers/components just get new property notifications
So inside your controllers/components you can use
this.get('notifications')
I was able to inject the addon to my specific service by creating a new initialiser with the code below:
export default {
name: 'inject-ember-notification-service',
initialize: function(container, app) {
app.inject('services:message', 'notifications', 'notification-messages:service');
}
};
Fiendishly obtuse, ember!
Thanks to you all for your input.
Much ink has flowed about Sf2 controller/container. I face with follow situation:
app/console container:debug security
...
> 4
[container] Information for service security.token_storage
Service Id security.token_interface
Class Symfony\Component\Security\Core\Authentication\Token ...
...
Public yes
LoginBundle\DefaultController.php
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class DefaultController extends Controller
{
public function indexAction()
{
dump(Controller::get('security.token_storage'));
...
works OK, obviously.
LoginBundle\UserUtilsController
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class UserUtilsController extends Controller
{
public function getRoleById()
{
dump(Controller::get('security.token_storage'));
...
throw: Error: Call to a member function get() on a non-object
In Sf2 Book - Service container I found:
In this example, the controller extends Symfony's base Controller, which gives you access to the service container itself. You can then use the get method to locate and retrieve the my_mailer service from the service container.
The misunderstanding is:
- Both controllers extends basic controller which itself extends ContainerAware which implements ContainerAwareInterface which set container.
- Both controllers access same public service container.
So, why the second controller it doesn't work?
I know that the question is old but I don't want to inject a controller as service and I think it is redundant and wrong to redeclare a public service in services.yml
Thank you in advance.
I found the answer myself and I want to share for every one is in same situation...
The UserUtilsController doesn't work because it's not working in this manner. The Symfony architecture is interesting if you get to know it.
LoginBundle\Controller\UserUtilsController
// For this job we don't need to extends any class..
class UserUtilsController
{
// but we need a property for injecting the service in it
private $token;
// Now let's inject service into our property $token
public function __construct($token)
{
$this->token = $token;
}
// It's not done but let pretend it is and let's use it
public function getRoleById()
{
...
return $this->token->getToken()->getRoles();
...
services.yml
#here it's the magic
services:
# this is a new services container
user.loggeduser_utils:
# this is my class (second class)
class: LoginBundle\Controller\UserUtilsController
# this is how I feed my _construct argument
arguments: ["#security.token_storage"]
So I just inject an existing service in my new class.
Now, to use this we must to call in first class:
LoginBundle\Controller\DefaultController.php
class DefaultController extends Controller
{
public function indexAction()
{
// because my class is now a service container we call in this way
$userRoleId = $this->get('user.loggeduser_utils');
...
This solution above is almost trivial simple AFTER understanding the Sf2 DI model.