6

I have a whole lot of configuration settings that I would like to be able to access throughout my application. For example, I have an entity repository with some custom queries. I would like to have a global parameter that sets the default "limit" to something like 10 records unless I specifically change it.

class ViewVersionRepository extends EntityRepository {

    /**
     * Get all the versions for the specified view id, ordered by modification time.
     * @param integer $id
     * @param integer $limit
     * @param integer $offset default 0
     * @return array
     */
    public function findByViewIdLimit($id, $limit = NULL, $offset = 0) {

        // this won't work because we don't have access to the container... ;(
        $limit = ($limit != NULL) ? $limit : $this->container->getParameter('cms.limit');

        return $this->createQueryBuilder('v')
            ->where('v.viewId = :id')
            ->orderBy('v.timeMod', 'DESC')
            ->setParameter('id', $id)
            ->setFirstResult($offset)
            ->setMaxResults($limit)
            ->getQuery()
            ->getResult();
    }

}

In my pre-Symfony life, I could easily set this in my parameters.yml:

cms.limit: 10

or create a constant for this in some app config file:

define('cms.limit', 10);

But in Symfony, if I use a parameter, how can I access that from my entity repository since the "container" is not available in an entity repository?

People say "pass it in as an argument" but honestly, that's just clutter and senseless work for every time I call the query. What's the point of having parameters if you can't access them!

Others say you have to construct some global service model (which I haven't understood yet either and seems like a HUGE effort for just fetching parameters).

So what is the "best" way to access global parameters like this in ANY context (including the one here)?

I don't want to access the kernel, but if that's the last resort, why is it "wrong" to get the parameter from the kernel like:

global $kernel;
$assetsManager = $kernel->getContainer()->get('acme_assets.assets_manager');‏

I can't believe it is this hard to access global parameters. I understand the idea of needing to be explicit about the dependencies. But in an application where the parameters will always be available, and you always want to use them as say a default, why isn't this standard and easy?


Update

Per the answers below I have turned my ViewVersionRepository into a service (that is what you were suggesting right?).

class ViewVersionRepository extends EntityRepository

{

    // do not evidently need to set $this->viewVersion either
    // protected $viewVersion;
    protected $limit;

    /**
     * Allow limit to be set by parameter, injected by service definition
     * @param $limit
     */
    public function setLimit($limit) {
        $this->limit = $limit;
    }



    /**
     * Get all the versions for the specified view id, ordered by modification time.
     * @param integer $id
     * @param integer $limit default from global parameter
     * @param integer $offset default 0
     * @return array
     */
    public function findByViewIdLimit($id, $limit = NULL, $offset = 0) {

        $limit = (!empty($limit)) ? $limit : $this->limit;

        return $this->createQueryBuilder('v')
            ->where('v.viewId = :id')
            ->orderBy('v.timeMod', 'DESC')
            ->setParameter('id', $id)
            ->setFirstResult($offset)
            ->setMaxResults($limit)
            ->getQuery()
            ->getResult();
    }

}

Add Service:

gutensite_cms.view_version_repository:
    class: Gutensite\CmsBundle\Entity\View\ViewVersionRepository
    factory_service: 'doctrine.orm.default_entity_manager'
    factory_method: 'getRepository'
    arguments:
        - 'Gutensite\CmsBundle\Entity\View\ViewVersion'
    calls:
        - [setLimit, ['%cms.limit.list.admin%']]

This results in an error complaining about the factory_service definition:

You have requested a non-existent service "doctrine.orm.default_entity_manager". Did you mean one of these: "doctrine.orm.cms_entity_manager", "doctrine.orm.billing_entity_manager"?

Fortunately, if I change factory_service to the suggested doctrine.orm.cms_entity_manager it works. Can someone explain why?

But then I get another error related to a method in the repository not existing (because the repository is broken). I thought that having the arguments in the service definition, means I needed to add a __construct($viewVersion) method to ViewVersionRepository to accept the arguments list. But that resulted in a nasty error that broke the repository. So I removed that because evidently this must be in the default Symfony EntityRepository (how am I supposed to know that?). I did keep a custom setLimit() method for the passing the global parameters into the call.

So now the service works... but that seems like SUCH a ton of work, just to get access to global parameters that should function as a DEFAULT if no parameters are passed into the method.

Why shouldn't we use a $GLOBAL or a constant from a config file in these sorts of situations where we are setting defaults? This doesn't break the dependency model, the custom variables can still be passed into the method, only we have a global app wide default. I don't understand why that is frowned upon...

Community
  • 1
  • 1
Chadwick Meyer
  • 7,041
  • 7
  • 44
  • 65

2 Answers2

9

It's worth while to study and understand how to create a repository as a service. Something like:

view_version_repository:
    class:  My\SomeBundle\Entity\ViewVersionRepository
    factory_service: 'doctrine.orm.default_entity_manager'
    factory_method:  'getRepository'
    arguments:  
        - 'My\SomeBundle\Entity\ViewVersion'
    calls:
         - [setLimit, ['%limit_parameter%']]

In your controller you would do:

$viewVersionRepository = $this->container->get('view_version_repository');

See docs: http://symfony.com/doc/current/components/dependency_injection/factories.html

The effort expended in learning how to use services will repay itself many times over.

Chadwick Meyer
  • 7,041
  • 7
  • 44
  • 65
Cerad
  • 48,157
  • 8
  • 90
  • 92
  • That's great. Ya I'm not opposed to setting a repository as a service (just a little foggy on how it all works, despite lots of reading and experimenting). It just seems like pretty soon we've got a ton of services for everything. Is that bad? – Chadwick Meyer May 16 '14 at 20:36
  • 1
    @ChadwickMeyer Not bad, awesome! Low coupling, high cohesion. Just as it should be… ;-) – nietonfir May 16 '14 at 21:04
  • 1
    To paraphrase @nietonfir, services are a good thing in Symfony 2. – Cerad May 16 '14 at 21:44
  • 2
    aiyayai... so it's expected to make lots of services. What's the rule of thumb? Whenever you need something to access global variables, you should make a service and pass in those variables as calls? – Chadwick Meyer May 16 '14 at 22:57
  • Globals variables should be injected. Doctrine repositories are a bit unusual in that they need to be created using a factory which in turn means that additional parameters need to be passed using setter (aka calls) injection. For most services, you can just pass your parameters directly to the constructor. – Cerad May 17 '14 at 00:56
  • So with something like this that will be a universal default, which could be overwritten by the function parameters passed in, but serve as a platform wide default, why exactly should we not use $GLOBALS or CONSTANTS in some central config file? – Chadwick Meyer May 19 '14 at 17:40
  • Can you explain, why you pass in the viewVersion entity via the `arguments`? Normally I would call this function via `$em->getRepository('GutensiteCmsBundle:View\ViewVersion')->findByViewIdLimit($id, 10, 0);` but if we make this a service, we would call it as a service like your example, so presumably it wouldn't know that it was a repository for the viewVersion entity? If I define the __construct($viewVersion) to accept this argument, will that cause problems if we call the repository the default way in other contexts? – Chadwick Meyer May 19 '14 at 18:49
  • Hmm. Don't mess with the repository constructor. The container is actually doing $em->getRepository('ViewVersion') then calling setLimit() on the returned repository object. You are not quite understanding the full process. Experiment a bit. – Cerad May 19 '14 at 19:10
  • Correct, I am not at all understanding the full process :D I am however reading a lot, but not seeing how it all ties together (the docs are very shallow on details). I can confirm that you should NOT add a __construct() to the viewVersionRepository, it results in a nasty error. But I assume I SHOULD at a setLimit function? Or else how else would the call from the service container work? But if that's the case, do you still pass in the ViewVersion as the argument? Does the Symfony repository constructor already accept that? How am I suppose to know that? – Chadwick Meyer May 19 '14 at 23:58
  • Yes you need to add a setLimit method to the ViewVersionRepository. And again, accessing a doctrine repository through the service container requires using the entity manager as a repository factory. Not sure how else to explain it. You have to pass the entity class name as an argument so the manager knows which repository to return. In this particular case, the repository class name is ignored though you get a waning if you omit it. – Cerad May 20 '14 at 00:16
  • I've updated my answer with the steps I've taken. After some fixes to some errors in the config suggested, does work. Yay! But that seems like SUCH a ton of work, just to get access to global parameters that should function as a DEFAULT if no parameters are passed into the method. Why again, shouldn't we use a $GLOBAL or a constant from a config file in these sorts of situations where we are setting defaults? This doesn't break the dependency model, the custom variables can still be passed into the method, only we have a global app wide default. I don't understand why that is frowned upon... – Chadwick Meyer May 20 '14 at 00:21
  • Also, let's say I need to call my ViewVersionRepository#findByViewIdLimit() from a method in my ViewRepository. In a controller when I call this as a service `$this->get('gutensite_cms.view_version_repository')->findByViewIdLimit()`, the global limit parameter is available from the service. But in ViewRepository, I don't have access to the service container, so I have to call `$version = $this->getEntityManager()->getRepository('GutensiteCmsBundle:View\ViewVersion')->findByViewIdLimit()`. However that won't have the global "limit" parameter... So what is the solution in contexts like this? – Chadwick Meyer May 20 '14 at 00:29
  • If you really need to access the ViewVersionRepository from the ViewRepository then create a service for the ViewRespository, add a setViewVersionRepository method and inject the ViewVersionRepository into it. Just take your time and get comfortable with using the dependency injection container. You can also also google things like "why globals are bad" and maybe read up a bit on automated testing. – Cerad May 20 '14 at 01:11
  • On second thought, I presume you shouldn't access one repository from another so my question is irrelevant. That should be the job of the controller to query each repo and then compile what it needs. – Chadwick Meyer May 20 '14 at 21:13
  • Are there use cases when GLOBALS and CONSTANTS are ever encouraged to be used then? – Chadwick Meyer May 20 '14 at 21:13
  • Question title clearly reads: `how to get parameter in entity repository` and thats why Google lead me here. But your answer wrongly says how to read parameter in Controller, not Entity Repository. If your answer is passing as parameter, how about separation of concerns?! – Peyman Mohamadpour Apr 09 '17 at 07:36
  • @Trix - Are you sure you commented the right answer? This answers shows how to inject a parameter into a repository. The controller code is just an example of how to access the repository. This document (created after this question) might also help: http://symfony.com/doc/current/best_practices/configuration.html#constants-vs-configuration-options – Cerad Apr 09 '17 at 14:26
1

I personally used a custom config class. You can import your config class wherever you need - entity, repository, controller etc.

To answer your question why "pass it as an argument" just study the dependency injection pattern. It is really a flexible and cool way to solve many of the problems when you want to maintain your project for a longer time.

Laurynas Mališauskas
  • 1,909
  • 1
  • 19
  • 34
  • 1
    Can you show an example of your config class and how you import it? – Chadwick Meyer May 16 '14 at 20:38
  • But basically you are saying, it's best to write redundant code in the controller that checks for a limit from the Request before using the default parameter setting, before calling the a repository query every time. That seems really bloated. Would be so much better to be able to do it ONCE in the findByViewIdLimit() function. I still allow $limit to be passed in, so as far as dependency, it is still flexible. But instead of hardcoding a limit of 10 in every single query, I use one central "default". We're talking global defaults here... – Chadwick Meyer May 16 '14 at 20:42
  • you can import that config class in your repository and then use its parameters in any function in the repository. Class imports are made everywhere in Symfony code - the first lines of controller for example. – Laurynas Mališauskas May 17 '14 at 16:40