Use SOLID to write Clean Code... Are you tired of theoretical post about how to do achieve some therm? So am I. <br> Instead, let's dive into real problems I came across while coding and let the code speak the theory between lines.
Today we try to add own config option to YAML of Easy Admin Bundle (without pull-request to the package).
Hindsight is 20/20.
Instead of writing about solution how to do and how awesome I am to know the solution right from the start of this page, I start right from the beginning, where I know nothing about it just like you.
I'm coding an open-sourced training platform build Symfony 4.2 and Doctrine 2.7 for Pehapkari community training. It's fully open-sourced on Github under the typical open-source name - Open Training.
Admin is just a CRUD to maintain few entities, so I use EasyAdminBundle to handle forms, grids, update, create, delete actions in controllers for me. Huge thanks to Javier Eguiluz for this amazingly simple and powerful idea.
There is Training
entity with name
and relation to TrainingTerm
entity:
<?php declare(strict_types=1);
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Training
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
* @var int
*/
private $id;
/**
* @ORM\Column(type="string", length=255)
* @var string
*/
private $name;
// ...
/**
* @ORM\OneToMany(targetEntity="App\Entity\TrainingTerm", mappedBy="training")
* @var TrainingTerm[]|ArrayCollection
*/
private $trainingTerms = [];
}
I want to edit this entity in administration, so I add it to config/packages/easy_admin.yaml
:
easy_admin:
entities:
Training:
class: 'App\Entity\Training'
This creates a grid and form with all the entity properties - name
and trainingTerms
. So, when I click Add in the admin I can change them both. But I want to change the name
only and handle TrainingTerm
entity in a standalone form.
Now what? I Google easy admin custom field form and after while I find Customize the Properties Displayed tutorial. It looks like exactly what I need.
easy_admin:
entities:
Training:
class: 'App\Entity\Training'
+ fields: ['name']
It works! The trainingTerms
property is hidden in the form.
After 2 hours I need to add price
.
<?php declare(strict_types=1);
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Training
{
// ...
+ /**
+ * @ORM\Column(type="integer")
+ * @var int
+ */
+ private $price;
Price is there, great! Now we can earn some money. I edit training in admin... but, where is the price?
Because I'm coding many other features I don't realize, there is memory-vendor-lock - a code smell, when after doing the A, you always have to remember the B. Do you see it? When I add a property, I always have a to add it to fields
in config.
easy_admin:
entities:
Training:
class: 'App\Entity\Training'
- fields: ['name']
+ fields: ['name', 'price']
If this is the only case, that would be ok-ish. But now there are 10 entities with 50 properties. How the hell will I remember to do this on every new property I add?
# ...
- fields: ['name', 'price']
+ fields: ['name', 'price', 'capacity']
And how can will anyone else find this out without me doing the code-review and remembering?
# ...
- fields: ['name', 'price', 'capacity']
+ fields: ['name', 'price', 'capacity', 'duration']
So much memory-leaks it hurts my neurons.
Life is not perfect and every code is legacy by the time you end the line with ;
.
There are no solutions. Just trade-offs.
I stop and think a bit. How can I write less code to prevent possible bugs and make changes as effective as possible? I see there are fewer properties to exclude than properties to include, by 1:10. It would not be perfect code, but still 10 times safer and more effective code. Worth it!
easy_admin:
entities:
Training:
class: 'App\Entity\Training'
- fields: ['name', 'price', 'capacity', 'duration', 'perex', 'description', 'place', 'trainer']
+ exclude_fields: ['trainingTerms']
But is that exclude_fields
or excluded_fields
or maybe skip_fields
? I want to see the documentation, so I Google easy admin bundle excludes fields. I find Exclude fields in list fields issue in EasyAdminBundle. I read it and see the content is not what I need. It looks like this option is not supported. I'm sad. What now?
Open-source packages are closed to extension more than you'd expect. To add one custom feature, you have to basically copy and extend the whole class or use reflection. It's not because it's difficult to create an extendable code, it's because nobody believes it can be done in a nice way. It can, just keep reading.
Being that suspicious I start my inner over-engineer voice:
EasyAdminExtension
and get the config and add exclude_fields
option"BetterEasyAdminBundle
that will be run before the EasyAdminBundle
and will pass parameters there"This might end-up wasting many hours on custom and useless solution (like "create own Doctrine" idea, true story). Instead I try to invest a bit more time and I continue the brainstorming:
Slightly better, but what if Javier doesn't like it? Or what if he's on holiday for 3 weeks? I know, it's summer and very rare to happen, but I have to finish the app in 2 weeks and I don't want to think about bugs like these in the meantime. The least I can do is to create an issue with this idea and my reasons for it.
I need a solution and I need it today. What can I do? No hacking, no pull-request, just looking for something in files:
Do you think this is just a random screen-shot not worth your attention?
'fields'
is lowercased and we want to focus on that only (no properties or methods with Fields
)*.php
because that would be probably place to extend/../vendor/easycorp
, because we want to hack into this packagefields
word in search; later I improve it to 'fields'
to narrow results, because we know it's a stringI still have no idea about the solution I'll pick. I'm only randomly looking for the light, blindfolded in a dark foggy forest. This is called creative chaos in coaching circles and it's the most important part of the client's work.
I scroll down a bit looking at both code and the file name. Suddenly, the fog starts slowly disappearing...
I notice *ConfigPass
suffix. Is that like CompilerPassInterface
, a collector-pattern used in Symfony to modify services in the container?
Being curious I open NormalizerConfigPass.php
file:
<?php
// ...
namespace EasyCorp\Bundle\EasyAdminBundle\Configuration;
use Symfony\Component\DependencyInjection\ContainerInterface;
class NormalizerConfigPass implements ConfigPassInterface
{
// ...
}
An interface! That's a good sign.
So I look for ConfigPassInterface
in somewhere else than just implements ConfigPassInterface
.
That doesn't work, so I try to look for ConfigPass
.
That doesn't work, so I try to look for any file, not just *.php
. That show as valuable, since services are defined in YAML or XML.
I see a tag: easyadmin.config_pass
. Let's look for that string:
Warmer! I've just found a collector pattern. To config, I look for service under easyadmin.config.manager
name - ConfigManager
and look for foreach
on collected services:
private function doProcessConfig($backendConfig): array
{
foreach ($this->configPasses as $configPass) {
$backendConfig = $configPass->process($backendConfig);
}
return $backendConfig;
}
Bingo! That means, when I register a service with easyadmin.config_pass
tag, I'll be able to read and modify the YAML configuration.
So I register a service:
services:
ExcludeFieldsConfigPass:
tags:
-
name: "easyadmin.config_pass"
priority: 120 # it took me more time to figure out if -100 or 0 or 100 or 1000 means "the first"
That does 1 thing:
fields
(value to be set) = entity properties − exclude_fields
(value I set in the config)
It allows me to do simplify config/packages/easy_admin.yaml
config:
easy_admin:
entities:
Training:
class: 'App\Entity\Training'
- fields: ['name', 'price', 'capacity', 'duration', 'perex', 'description', 'place', 'trainer']
+ exclude_fields: ['trainingTerms']
You can see full code of ExcludeFieldsConfigPass
on Github.
Very smart move Javier - thank you!
And that's all folks. I hope I've shown you how to approach problems and how to find a way in situations you're the first time in. The same way I don't memorize Wikipedia and just Google it instead, I don't remember solutions to 100 PHP problems, but have a couple of algorithms to approach problem solving.
Btw, are you coming to Human Level AI Conference in Prague this weekend? I'll be there and I'd be happy if you stop me and say Hi!
Happy solving!
Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!