Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Flex Extras

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $8.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Now that we're on Symfony 4 with Flex, I have three cool things to show you.

Repositories as a Service

Start by opening GenusController: find listAction. Ah yes: this is a very classic setup: get the entity manager, get the repository, then call a method on it.

One of the annoying things is that - unless you add a bunch of extra config - repositories are not services and can not be autowired. Boo!

Well... that's not true anymore! Want your repository to be a service? Just make two changes. First, extend a new base class: ServiceEntityRepository.

... lines 1 - 5
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
... lines 7 - 10
class GenusRepository extends ServiceEntityRepository
{
... lines 13 - 50
}

And second, override the __construct() function. But remove the $entityClass argument.

Tip

Make sure the type-hint for the first argument is RegistryInterface not ManagerRegistry.

In the parent call, use Genus::class.

... lines 1 - 12
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Genus::class);
}
... lines 17 - 52

That might look weird at first... but with those two small changes, your repository is already being auto-registered as a service! Yep, back in listAction, add a new argument: GenusRepository $genusRepository. Use that below instead of fetching the EntityManager.

... lines 1 - 18
class GenusController extends Controller
{
... lines 21 - 66
public function listAction(GenusRepository $genusRepository)
{
$genuses = $genusRepository
->findAllPublishedOrderedByRecentlyActive();
... lines 71 - 74
}
... lines 76 - 141
}

And that's it! Go to that page in your browser: /genus. Beautiful! Make that same change to your other repository classes when you want to.

Fixtures as Services

Ok, cool thing #2: our fixtures are broken. Well... that's not the cool part. They're broken because we removed Alice, so everything explodes:

But, there's even more going on. Find your composer.json file and make sure the version constraint is ^3.0.

81 lines composer.json
{
... lines 2 - 42
"require-dev": {
... lines 44 - 45
"doctrine/doctrine-fixtures-bundle": "^3.0"
},
... lines 48 - 79
}

Then, run:

composer update doctrine/doctrine-fixtures-bundle

Version 3 of this bundle is all new... but not in a "broke everything" kind of way. Before, fixture classes were loaded because they lived in an exact directory: usually DataFixtures\ORM in your bundle. And if you needed to access services, you extended ContainerAwareFixture and fetched them directly from the container.

Well, no more! In the new version, your fixtures are services, and so they act like everything else. You can even put them anywhere.

When Composer finishes, download one more package:

composer require fzaninotto/faker

Tip

Even better would be composer require fzaninotto/faker --dev!

This isn't needed by DoctrineFixturesBundle, but we are going to use it. In fact, if you downloaded the course code, you should have a tutorial/ directory with an AllFixtures.php file inside. Copy that and put it directly into DataFixtures.

... lines 1 - 15
class AllFixtures extends Fixture
{
... lines 18 - 22
public function load(ObjectManager $manager)
{
$this->faker = Factory::create();
$this->addSubFamily($manager);
$this->addGenus($manager);
$this->addGenusNote($manager);
$this->addUser($manager);
$this->addGenusScientist($manager);
$manager->flush();
}
... lines 34 - 140
}

Then, delete the old ORM directory. This is our new fixture class: all we need to do is extend Fixture from the bundle, and the command instantly recognizes it. If you need services, just add a constructor and use autowiring!

Let's go check on Faker. Ah, it's done! Inside the class, Faker allows me to generate really nice, random values. Does it work? Reload the fixtures:

./bin/console doctrine:fixtures:load

It sees our class immediately and... it works! Fixtures are services... and they work great.

MakerBundle

Ready for one last cool thing? Run:

composer require maker --dev

This installs the MakerBundle: Symfony's new code generator. Code generation is of course optional. But with this bundle, you'll be able to develop new features faster than ever. Need a console command, an event subscriber or a Twig extension? Yep, there's a command for that.

What's everything it can do? Run:

./bin/console list make

Right now, it has about 10 commands - but there are a lot more planned: this bundle is only about 1 month old!

Let's try one of these commands!

./bin/console make:voter

Call it RandomAccessVoter: we'll create a voter that randomly gives us access. Fun! Open the new class in src/Security/Voter. This comes pre-generated with real-world example code. In supports(), return $attribute === 'RANDOM_ACCESS'. Our voter will vote when someone calls isGranted() with RANDOM_ACCESS.

... lines 1 - 8
class RandomAccessVoter extends Voter
{
protected function supports($attribute, $subject)
{
return $attribute === 'RANDOM_ACCESS';
}
... lines 15 - 19
}

Then, for voteOnAttribute(), return random_int(0, 10) > 5.

... lines 1 - 15
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
return random_int(0, 10) > 5;
}
... lines 20 - 21

Now we need to go and update some configuration, right? No! This class is already being used! Open GenusController and... above newAction(), add @IsGranted("RANDOM_ACCESS").

... lines 1 - 19
class GenusController extends Controller
{
/**
... line 23
* @IsGranted("RANDOM_ACCESS")
*/
public function newAction()
... lines 27 - 143
}

Done! Try it: go to /genus/new. Ha! It sent us to the login page - that proves its working. Login with iliketurtles and... access granted! Refresh - granted! Refresh - denied!

All that by running 1 command and changing about 3 lines. Welcome to Symfony 4.

Let's go Symfony 4!

Hey, we're done! Upgrading to the Flex structure is work, but I hope you're as happy as I am about the result! To go further with Flex and Symfony 4, check out our Symfony Track: we're going to start a project with Flex and really do things right.

All right guys. Seeya next time!

Leave a comment!

24
Login or Register to join the conversation
Icapps N. Avatar
Icapps N. Avatar Icapps N. | posted 2 years ago

Finally finished upgrading a massive project from 3.4 till 4.4.20 (took 1 month) DI rewrite and config can become very time consuming on big projects. Tutorial is still good @ this date!

1 Reply

Thanks Danny! I happy to hear you could upgrade your project. Cheers!

Reply
Default user avatar
Default user avatar Mauro Chojrin | posted 3 years ago | edited

Hey there! Just finished migrating an old sf3 app following this guide. Awesome!

Thank you very much!

1 Reply

Wooo! Congrats - I know it's a BIG task :)

Reply
Default user avatar
Default user avatar Mauro Chojrin | weaverryan | posted 3 years ago

In did! I've been fighting getting out of FOSUserBundle into symfony/security (My app hadn't been updated in a LONG time :p).

Anyway, one thing I don't understand is why it doesn't provide the nice CLI tools the FOS one had... I just coded a rudimentary version of a couple myself... perhpaps and I'll create a PR for them

Reply

Hey Mauro Chojrin!

Hmm, that's a good point. Which commands do you use? I would find a command to add/remove a role useful.. but i'm not sure what else. Change password really should be done by the user... and the create user command was never very reliable anyways (it worked until you added new required properties to your entity). Anyways, let me know - the commands are not something I'd really thought much about before.

Cheers!

Reply
Default user avatar
Default user avatar Mauro Chojrin | weaverryan | posted 3 years ago

Well... so far I created just two I really needed: create a user and add a role to a user.

It makes sense in my context since I don't have open registration. It's more of an intranet for a very small user base so it's actually to make my own life easier :)

I'll do some research into publishing it in case someone else can benefit from it.

Cheers!

Reply
Default user avatar
Default user avatar toporovvv | posted 5 years ago

Great endpoint for Symfony 3 track. Thank you guys for awesome course!

1 Reply

Thanks! Let's keep learning :)

Reply
KevinC Avatar

Good day. I just migrated a massive 3.x sf project to 4.4 and moved to the new flex directory structure. I moved my ./app/Resources/FOSUserBundle/ to the new location ./templates/bundles/FOSUserBundle/ and none of my template overrides are getting picked up. I'm only seeing the FOSUserBundle default templates. Would love any suggestions as to what I missed in the migration. Btw, I'm on "friendsofsymfony/user-bundle": "^2.1"

Reply

Hey @Kevin!

Congrats on the upgrade! The template overriding can be tricky... as if it doesn't work, it's hard to debug. Your path looks good to me. Other than the FOSUserBundle overrides, are your other templates loading correctly from templates/?

One other thing to look at, in 4.4, you should have a bin/console debug:twig command. One of the things it shows you are Twig loader paths. You should see something like:

@FOSUser    templates/bundles/FOSUserBundle/
@FOSUser    vendor/friendsofsymfony/user-bundle/Resources/views/

If you don't see that first entry, then yea, something is wrong... but I don't know what. Definitely clear your cache if you haven't already. Internally, when the bundle renders templates, it uses this @FOSUser prefix - example https://github.com/FriendsOfSymfony/FOSUserBundle/blob/89c18b6944adb12541f5b3b1ab39287b5336b375/Controller/RegistrationController.php#L167 - and then Symfony should register a Twig path/alias for it automatically to point to your templates/bundles/FOSUserBundle/ directory.

Let me know what you find out!

Cheers!

Reply
KevinC Avatar

Ryan,
I heard from @stof over on github he straightened me out. I still had my templates down inside templates/bundles/FOSUserBundle/views/*, had to move them up out of the views subdir to get it wired up properly. Thanks for your quick responses and hopefully this helps someone else down the road.

-Kevin

Reply
KevinC Avatar

Thanks for the tips Ryan!
I do see the twig loader paths as:

@FOSUser             templates/bundles/FOSUserBundle/                          
                     vendor/friendsofsymfony/user-bundle/Resources/views/   

and I also see this strange one with an ! (exclamation)

@!FOSUser            vendor/friendsofsymfony/user-bundle/Resources/views/

In fact, I see this strange @! notation in many areas, could this be an issue?

@Doctrine            vendor/doctrine/doctrine-bundle/Resources/views/          
@!Doctrine           vendor/doctrine/doctrine-bundle/Resources/views/          
@Twig                templates/bundles/TwigBundle/                             
                     vendor/symfony/twig-bundle/Resources/views/               
                                                                                 
@!Twig               vendor/symfony/twig-bundle/Resources/views/              

I'm not able to see my dev _errors/ twig templates either. I'm unable to log in to my app atm (something about created_at column, which I'm working on now) so I can't test any other templates yet.

Thank you,
Kevin

Reply
Kakha K. Avatar
Kakha K. Avatar Kakha K. | posted 3 years ago

Thanks!

Reply
Default user avatar
Default user avatar Loïc Boutter | posted 3 years ago | edited

Hello, great course! It will be very useful!
I just wanted to point out that you probably want to install maker-bundle in dev mode only.
composer require --dev maker

Reply

Hey Loïc!

You're totally correct! We don't need Maker in prod environment at all :) Thank you for letting us know! I just fixed the command in: https://github.com/knpunive...

Cheers!

Reply
Default user avatar
Default user avatar notronwest | posted 5 years ago

Lets say I wanted all of my entities to extend a generic Repository (e.g. BaseRepository). My end goal is to override the generic find($id) function for all of my repositories so that if the passed in $id doesn't return a valid entity I return a new entity (with null values).

In your code example, GenusRepository extends ServiceEntityRepository and in the GenusRepository you define the Genus::class explicitly. However, in my example, I would like to have my BaseRepository receive a dynamic entity. So I added an argument:

public function __construct(RegistryInterface $registry, ClassMetadata $entityClass)

However, I get an error:

Cannot autowire service "App\BaseRepository": argument "$entityClass" of method "__construct()" references class "Doctrine\ORM\Mapping\ClassMetadata" but no such service exists. It cannot be auto-registered because it is from a different root namespace.

Is there a better way to do this?

Reply

Hey notronwest

There is another way to do that. You can change the default repository class binded to all your entities in your "doctrine.yaml" configuration file, something like this:


doctrine:
    orm:
        entity_managers:
            default:
                default_repository_class: Repository\BaseEntityRepository (change this)

You can read more info about all configurable options here: http://symfony.com/doc/current/reference/configuration/doctrine.html

Cheers!

Reply
Trafficmanagertech Avatar
Trafficmanagertech Avatar Trafficmanagertech | posted 5 years ago

Do you think we should also inject Doctrine in the controller with `RegistryInterface $doctrine` instead of $this->getDoctrine()?
This is my last question hehe, great course btw

Reply

Hey Trafficmanagertech

Usually you don't need Doctrine itself, what you usually need is a repository or the entity manager, but if for some reason you need it, I believe you can inject it directly to a controller's action

Cheers!

1 Reply

This is something we talk about on the core team as well. We're still recommending $this->getDoctrine() because it exists, and you get nice auto-completion and it's easy. But as Diego said, you can now autowire repositories, and that handles many of the cases you had before.

Cheers!

1 Reply
Default user avatar
Default user avatar Joachim | posted 5 years ago | edited

Is this still the recommended way for repositories as service?


public function __construct(ManagerRegistry $registry)
{
    parent::__construct($registry, Genus::class);
}

I'm asking because the last update to the DoctrineBundle removed the alias for ManagerRegistry leaving only the RegistryInterface alias. This change was reverted in v1.9.1 due to the obvious BC break (i think Ryan was involved too).
Also the official docs for sf4 don't use the ManagerRegistry but the RegistryInterface instead.


public function __construct(RegistryInterface $registry)
{
    parent::__construct($registry, Product::class);
}

https://symfony.com/doc/current/doctrine.html#querying-for-objects-the-repository

Maybe you might want to adjust the Script to reflect this or add some sort of hint to be consistent with the documentation.

Reply
Default user avatar

Sorry messed up the formatting, should've registered to edit it i guess.

Reply

Hey Joachim!

No worries - you were SO close to having it right (closer than most) - I cleaned it up :).

And actually, yea! We should update. I reverted that change, but *eventually* it will probably be removed. I'll add it to our list!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/doctrine-bundle": "^1.6", // 1.8.1
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.2
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.1
        "doctrine/orm": "^2.5", // v2.7.2
        "fzaninotto/faker": "^1.7", // v1.7.1
        "knplabs/knp-markdown-bundle": "^1.4", // 1.6.0
        "sensio/framework-extra-bundle": "^5.0", // v5.1.3
        "stof/doctrine-extensions-bundle": "dev-master", // dev-master
        "symfony/asset": "^4.0", // v4.0.1
        "symfony/console": "^4.0", // v4.0.1
        "symfony/flex": "^1.0", // v1.9.10
        "symfony/form": "^4.0", // v4.0.1
        "symfony/framework-bundle": "^4.0", // v4.0.1
        "symfony/lts": "^4@dev", // dev-master
        "symfony/maker-bundle": "^1.0", // v1.0.2
        "symfony/monolog-bundle": "^3.1", // v3.1.2
        "symfony/polyfill-apcu": "^1.0", // v1.6.0
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/security-bundle": "^4.0", // v4.0.1
        "symfony/security-csrf": "^4.0",
        "symfony/swiftmailer-bundle": "^3.1", // v3.1.6
        "symfony/translation": "^4.0", // v4.0.1
        "symfony/twig-bundle": "^4.0", // v4.0.1
        "symfony/validator": "^4.0", // v4.0.1
        "symfony/web-server-bundle": "^4.0", // v4.0.1
        "symfony/yaml": "^4.0" // v4.0.1
    },
    "require-dev": {
        "symfony/dotenv": "^4.0", // v4.0.1
        "symfony/phpunit-bridge": "^4.0", // v4.0.1
        "doctrine/doctrine-fixtures-bundle": "^3.0" // 3.0.2
    }
}
userVoice