gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
SOLID starts with the Single-Responsibility Principle or SRP. SRP says:
A module should have only one reason to change.
Um, huh? This sounds... a little too "fluffy" to be actually useful.
Let's... try again with a... somewhat simpler definition:
A function or class should be responsible for only one task... or should have only one "responsibility".
Better. But... what is a "responsibility" exactly? And why is this rule helpful?
On an even simpler level, what SRP is really trying to say is:
Gather together the things that change for the same reason and separate things that change for different reasons.
We'll talk more about this definition later, but keep it in mind.
And what problem is SRP trying to help us solve? In theory, if we organize our code into units that all change for the same reason, then when we get a new feature or change request, we will only need to modify one class... instead of making 10 changes to 10 different files... and trying not to break things along the way.
Enough defining stuff! Let's jump into an example. On your browser, click "Sign Up". As you can see, our app has a registration form! Open src/Controller/RegistrationController.php
to see the code behind this. Most of the logic for saving the user is in this UserManager::register()
method. Hold Cmd or Ctrl to jump into this: it lives at src/Manager/UserManager.php
.
... lines 1 - 8 | |
class UserManager | |
{ | |
... lines 11 - 19 | |
public function register(User $user, string $plainPassword): void | |
{ | |
$user->setPassword( | |
$this->passwordEncoder->encodePassword($user, $plainPassword) | |
); | |
$this->entityManager->persist($user); | |
$this->entityManager->flush(); | |
} | |
} |
This method hashes the user's password... and then saves the user to the database. Awesome!
But now... we've received a change request! The product manager of Sasquatch Sightings - a suspiciously hairy person - would like us to send a confirmation email after registration to verify the user's email address.
To understand SRP, let's implement this the wrong way first. Well "wrong" according to SRP.
Side note: we're going to build a simple email confirmation system by hand. If you have this need in a real project, check out symfonycasts/verify-email-bundle.
Anyways, the easiest way I can see to add this feature is to add the logic right inside UserManager::register()
... because we will only have to touch one file and it will guarantee that anything that calls this method will definitely trigger the confirmation email.
At the bottom of this class, I'm going to start by pasting in a private function called createToken()
. You can copy this from the code block on this page. This generates a random string that we will include in the confirmation link.
... lines 1 - 8 | |
class UserManager | |
{ | |
... lines 11 - 29 | |
private function createToken(): string | |
{ | |
return rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '='); | |
} | |
} |
Up in register, generate a new token $token = $this->createToken()
... and then set it on the user: $user->setConfirmationToken($token)
.
... lines 1 - 19 | |
public function register(User $user, string $plainPassword): void | |
{ | |
$token = $this->createToken(); | |
$user->setConfirmationToken($token); | |
... lines 24 - 30 | |
} | |
... lines 32 - 38 |
Before I started recording - if you look at the User.php
file - I already created a $confirmationToken
property that saves to the database. So thanks to the new code, when a user registers, they will now have a random confirmation token saved onto their row in the database.
... lines 1 - 15 | |
class User implements UserInterface | |
{ | |
... lines 18 - 60 | |
/** | |
* @ORM\Column(type="string", unique=true, nullable=true) | |
*/ | |
private $confirmationToken; | |
... lines 65 - 223 | |
} |
Back in RegistrationController
... if you scroll down a bit, I've also already built a confirmation action to confirm their email. A user just needs to go to this pre-made route - where the {token}
in the URL matches the confirmationToken
that we've set onto their User
record - and... bam! They'll be verified!
... lines 1 - 13 | |
class RegistrationController extends AbstractController | |
{ | |
... lines 16 - 43 | |
/** | |
* @Route("/confirm/{token}", name="check_confirmation_link") | |
*/ | |
public function confirmAction(string $token, UserRepository $userRepository, EntityManagerInterface $entityManager) | |
{ | |
$user = $userRepository->findOneBy(['confirmationToken' => $token]); | |
if (!$user) { | |
throw $this->createNotFoundException(sprintf('The user with confirmation token "%s" does not exist', $token)); | |
} | |
$user->setConfirmationToken(null); | |
$entityManager->flush(); | |
$this->addFlash('success', 'Your email is confirmed! Let\'s go confirm some Bigfoot!'); | |
return $this->redirectToRoute('app_homepage'); | |
} | |
} |
So back in UserManager
, we have two jobs left. First, we need to generate an absolute URL to the confirmAction
that contains their token. And second, we need to send an email to the user with that URL inside.
Let's generate the URL first. Up in the constructor, autowire RouterInterface $router
. I'll hit Alt + Enter and go to "Initialize properties" to create that property and set it.
... lines 1 - 7 | |
use Symfony\Component\Routing\RouterInterface; | |
... lines 9 - 10 | |
class UserManager | |
{ | |
... lines 13 - 14 | |
private RouterInterface $router; | |
... line 16 | |
public function __construct(UserPasswordEncoderInterface $passwordEncoder, EntityManagerInterface $entityManager, RouterInterface $router) | |
{ | |
... lines 19 - 20 | |
$this->router = $router; | |
} | |
... lines 23 - 44 | |
} |
Now, below, say $confirmationLink = $this->router->generate()
and... the name of our route... is check_confirmation_link
. Use that. For the second argument, pass token
set to $user->getConfirmationToken()
. And because this URL will go into an email, it needs to be absolute. Pass a third argument to trigger that: UrlGeneratorInterface::ABSOLUTE_URL
.
... lines 1 - 23 | |
public function register(User $user, string $plainPassword): void | |
{ | |
... lines 26 - 28 | |
$confirmationLink = $this->router->generate('check_confirmation_link', [ | |
'token' => $user->getConfirmationToken() | |
], UrlGeneratorInterface::ABSOLUTE_URL); | |
... lines 32 - 38 | |
} | |
... lines 40 - 46 |
Now, let's send the email! On top, add one more argument - MailerInterface $mailer
and use the same Alt + Enter, "Initialize properties", trick to create that property and set it.
... lines 1 - 6 | |
use Symfony\Component\Mailer\MailerInterface; | |
... lines 8 - 11 | |
class UserManager | |
{ | |
... lines 14 - 16 | |
private MailerInterface $mailer; | |
... line 18 | |
public function __construct(UserPasswordEncoderInterface $passwordEncoder, EntityManagerInterface $entityManager, RouterInterface $router, MailerInterface $mailer) | |
{ | |
... lines 21 - 23 | |
$this->mailer = $mailer; | |
} | |
... lines 26 - 47 | |
} |
Beautiful! Below, I'll paste in some email generation code. I'll also re-type the l
on TemplatedEmail
and hit tab so that PhpStorm adds the use
statement on top for me.
... lines 1 - 6 | |
use Symfony\Bridge\Twig\Mime\TemplatedEmail; | |
... lines 8 - 12 | |
class UserManager | |
{ | |
... lines 15 - 27 | |
public function register(User $user, string $plainPassword): void | |
{ | |
... lines 30 - 36 | |
$confirmationEmail = (new TemplatedEmail()) | |
->from('staff@example.com') | |
->to($user->getEmail()) | |
->subject('Confirm your account') | |
->htmlTemplate('emails/registration_confirmation.html.twig') | |
->context([ | |
'confirmationLink' => $confirmationLink | |
]); | |
... lines 45 - 51 | |
} | |
... lines 53 - 57 | |
} |
This creates an email to this user, from this address... and the template it references already exists. You can see it in: templates/emails/registration_confirmation.html.twig
.
{% apply inline_css %} | |
<html lang="en"> | |
... lines 4 - 42 | |
<body> | |
<div class="body"> | |
... lines 45 - 50 | |
<div class="content"> | |
<h1 class="text-center">Nice to meet you %name%!</h1> | |
<p class="block"> | |
Please <a href="{{ confirmationLink }}">Confirm your account</a>. | |
</p> | |
<p class="block"> | |
Or go directly to this URL: {{ confirmationLink }} | |
</p> | |
</div> | |
... lines 60 - 66 | |
</div> | |
</body> | |
</html> | |
{% endapply %} |
We're passing a confirmationLink
variable... and that is rendered inside the email.
Finally, all the way at the bottom of register()
... so after we know that the user has saved successfully, deliver the mail with: $this->mailer->send($confirmationEmail)
.
... lines 1 - 27 | |
public function register(User $user, string $plainPassword): void | |
{ | |
... lines 30 - 52 | |
$this->mailer->send($confirmationEmail); | |
} | |
... lines 55 - 61 |
Alright! We did it! And we can even try this! Back at the registration page, register as a new user... any password, hit enter and... awesome! It looks like it worked!
Now, the project is not configured to actually deliver the email. But we can see what that imaginary email would have looked like by going down to the web debug toolbar, clicking any of these links to go to the profiler... hitting "last 10"... then clicking to get into the profiler for the POST request that we just made to the registration form.
On the left, click into the "Email" section. There's our email! You can even look at its HTML. I'm going to steal the confirmation link... pop it into a new tab and... our email is confirmed! Mission accomplished!
And, all of our code is centralized into one method. But... we did just violate SRP: our UserManager
class now has too many responsibilities! But what do I mean by the word "responsibility"? And what are the responsibilities that this class has? And what's the problem with violating SRP anyways? And does the influence of gravity extend out forever?
Let's answers most of these questions next.
Hey Abelardo L.
This is a real good question. Code organization is all about giving clarity to you and your team of how things are related to each other, or to easily find a piece of code/class/package you need to work with. In the case of Symfony, I like how things are organized, for example, I know that inside the Entity/
folder I'll find all the entities in my system. Perhaps in a big project it may make sense to add inner folders so you can organize a bit better what entities belongs to a specific module (sales). It's also important to consider if your project it's private or public, if it's public you should expect people adding code to it, so you may want to organize things in a different way but the end goal is the same - give clarity to other of how things relates to each other
If you want to know more about code organization I recommend this article https://medium.com/@egonelbre/thoughts-on-code-organization-c668e7cc4b96
Cheers!
It's very weird the enterprise hierarchical varies; so, this guarantees that the structure of my code barely changes too.
Yea... this took us WAY too long - I even talk about that in the first chapter! https://symfonycasts.com/sc...
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99.1
"doctrine/annotations": "^1.0", // 1.12.1
"doctrine/doctrine-bundle": "^2", // 2.3.1
"doctrine/doctrine-migrations-bundle": "^3", // 3.1.1
"doctrine/orm": "^2", // 2.8.4
"knplabs/knp-time-bundle": "^1.15", // v1.16.0
"phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
"sensio/framework-extra-bundle": "^6.0", // v6.1.2
"symfony/console": "5.2.*", // v5.2.6
"symfony/dotenv": "5.2.*", // v5.2.4
"symfony/flex": "^1.9", // v1.18.7
"symfony/form": "5.2.*", // v5.2.6
"symfony/framework-bundle": "5.2.*", // v5.2.6
"symfony/http-client": "5.2.*", // v5.2.6
"symfony/mailer": "5.2.*", // v5.2.6
"symfony/property-access": "5.2.*", // v5.2.4
"symfony/property-info": "5.2.*", // v5.2.4
"symfony/security-bundle": "5.2.*", // v5.2.6
"symfony/serializer": "5.2.*", // v5.2.4
"symfony/twig-bundle": "5.2.*", // v5.2.4
"symfony/validator": "5.2.*", // v5.2.6
"symfony/webpack-encore-bundle": "^1.6", // v1.11.1
"symfony/yaml": "5.2.*", // v5.2.5
"twig/cssinliner-extra": "^3.3", // v3.3.0
"twig/extra-bundle": "^2.12|^3.0", // v3.3.0
"twig/twig": "^2.12|^3.0" // v3.3.0
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.2", // 3.4.0
"fakerphp/faker": "^1.13", // v1.14.1
"symfony/debug-bundle": "^5.2", // v5.2.4
"symfony/maker-bundle": "^1.13", // v1.30.2
"symfony/monolog-bundle": "^3.0", // v3.7.0
"symfony/stopwatch": "^5.2", // v5.2.4
"symfony/var-dumper": "^5.2", // v5.2.6
"symfony/web-profiler-bundle": "^5.2" // v5.2.6
}
}
Hi there,
Is a good practice to reproduce the enterprise hierarchical scheme at the code?
For instance, if my company has a Sales department, should I create a dir inside my project called "sales" for keeping my code related to sales?
I think it's a good idea since if somebody from Sales department would like to make a change inside my code, I would know where to go.
What are your thoughts about this approach?
Regards.