Dependency injection in Symfony
You work with Symfony, but the concept of dependency injection is a little blurry for you? Find out how to take advantage of the component reading this article.
Xpression is a simple parser which converts textual expression (DSL) into logical one (specification pattern). Here's an overview of Xpression's functionnalities.
There is some expression examples:
Age must be equal to 26
.
age=26
Age must be greater than or equal to 20
(included) and less than 30
(excluded).
age≥20&age<30
Operators supported by bridges:
Operator | Syntax | Examples | ORM | ODM | ArrayCollection | Closure |
---|---|---|---|---|---|---|
equal | = | param=value | X | X | X | X |
not equal | != ≠ | param!=value param≠value | X | X | X | X |
greater than | > | param>value | X | X | X | X |
greater equal than | >= ≥ | param>=value param≥value | X | X | X | X |
less than | < | param<value | X | X | X | X |
less equal than | <= ≤ | param<=value param≤value | X | X | X | X |
in | [ ] | param[value1,value2] | X | X | X | X |
contains | {% raw %}{{{% endraw %} {% raw %}}}{% endraw %} | {% raw %}param{{value}}{% endraw %} | X | X | X | |
not contains | {% raw %}!{{{% endraw %} {% raw %}}}{% endraw %} | {% raw %}param!{{value}}{% endraw %} | X | X | X | |
and | & | param>1¶m<10 | X | X | X | X |
not and | !& | param>1!¶m<10 | X | X | ||
or | | | param>1|param<10 | X | X | X | X |
not or | !| | param>1!|param<10 | X | |||
exclusive or | ^| ⊕ | param>1^|param<10 param>1⊕param<10 | X |
Yes, the library provides some bridges like doctrine
ORM
,ODM
andcommon
(collections filter).
Pay attention to the composition priority operator (&
, !&
, |
, !|
, ⊕
).
The bigger priority applies first.
and
: 15not and
: 14or
: 10exclusive or
: 9not or
: 8Use parenthesis (
)
to group the expressions as you need.
For example, this expression will select the Raccoon
or the Schizo
with more than 100 points.
planet='Raccoon'|name='Schizo'&point>100
is equal to planet='Raccoon'|(name='Schizo'&point>100)
But the following expression will select Raccoon
with more than 100 points or Schizo
with more than 100 points.
(planet='Raccoon'|name='Schizo')&point>100
Let's see with which cases we should use this library.
In order to have a specification we will use ClosureExpressionBuilder
.
In fact, this class builds a callback with the input expression.
<?php
use Symftony\Xpression\Expr\ClosureExpressionBuilder;
use Symftony\Xpression\Parser;
// class with getter
class Astronaut {
private $name;
private $planet;
private $points;
private $rank;
public function __construct($name, $planet, $points, $rank)
{
$this->name = $name;
$this->planet = $planet;
$this->points = $points;
$this->rank = $rank;
}
public function getName()
{
return $this->name;
}
public function getPlanet()
{
return $this->planet;
}
public function getPoints()
{
return $this->points;
}
public function getRank()
{
return $this->rank;
}
}
// objet with public property
$astronaut1 = new \stdClass();
$astronaut1->name = 'Mehdy';
$astronaut1->planet = 'Raccoons of Asgard';
$astronaut1->points = 675;
$astronaut1->rank = 'Captain';
$query = 'planet{{Raccoons}}|points≥1000';
$parser = new Parser(new ClosureExpressionBuilder());
$specification = $parser->parse($query);
$specification(['name' => 'Arnaud', 'planet' => 'Duck Invaders', 'points' => 785, 'rank' => 'Fleet Captain']); // false
$specification($astronaut1);// true
$specification(new Astronaut('Ilan', 'Donut Factory', 1325, 'Commodore')); // true
As you see, the specification can be called with an associative array, an object with public properties and an object with getters.
We filter an associative array.We use again
ClosureExpressionBuilder`.
This is the dataset we use for the following examples:
<?php $astronauts = [ ['name' => 'Jonathan', 'planet' => 'Duck Invaders', 'points' => 5505, 'rank' => 'Fleet Admiral'], ['name' => 'Thierry', 'planet' => 'Duck Invaders', 'points' => 2555, 'rank'=> 'Vice Admiral'], ['name' => 'Vincent', 'planet' => 'Donut Factory', 'points' => 1885, 'rank' => 'Rear Admiral'], ['name' => 'Rémy', 'planet' => 'Schizo Cats', 'points' => 1810, 'rank' => 'Rear Admiral'], ['name' => 'Charles Eric', 'planet' => 'Donut Factory', 'points' => 1385, 'rank' => 'Commodore'], ['name' => 'Ilan', 'planet' => 'Donut Factory', 'points' => 1325, 'rank' => 'Commodore'], ['name' => 'Alexandre', 'planet' => 'Schizo Cats', 'points' => 1135, 'rank' => 'Commodore'], ['name' => 'Noel', 'planet' => 'Duck Invaders', 'points' => 960, 'rank' => 'Fleet Captain'], ['name' => 'Damien', 'planet' => 'Donut Factory', 'points' => 925, 'rank' => 'Fleet Captain'], ['name' => 'Quentin', 'planet' => 'Donut Factory', 'points' => 910, 'rank' => 'Fleet Captain'], ['name' => 'Martin', 'planet' => 'Schizo Cats', 'points' => 860, 'rank' => 'Fleet Captain'], ['name' => 'Carl', 'planet' => 'Donut Factory', 'points' => 800, 'rank' => 'Fleet Captain'], ['name' => 'Arnaud', 'planet' => 'Duck Invaders', 'points' => 785, 'rank' => 'Fleet Captain'], ['name' => 'Alexandre', 'planet' => 'Donut Factory', 'points' => 785, 'rank' => 'Fleet Captain'], ['name' => 'Thibaud', 'planet' => 'Raccoons of Asgard', 'points' => 760, 'rank' => 'Fleet Captain'], ['name' => 'Romain', 'planet' => 'Donut Factory', 'points' => 735, 'rank' => 'Captain'], ['name' => 'Julie', 'planet' => 'Donut Factory', 'points' => 735, 'rank' => 'Captain'], ['name' => 'Cedric', 'planet' => 'Donut Factory', 'points' => 700, 'rank' => 'Captain'], ['name' => 'Mehdy', 'planet' => 'Raccoons of Asgard', 'points' => 675, 'rank' => 'Captain'], ['name' => 'Romain', 'planet' => 'Raccoons of Asgard', 'points' => 550, 'rank' => 'Captain'], ];
I want to get a 'Raccoons' astronaut.
<?php use Symftony\Xpression\Expr\ClosureExpressionBuilder; use Symftony\Xpression\Parser; // dataset $astronauts = [...]; $query = 'planet{{Raccoons}}'; $parser = new Parser(new ClosureExpressionBuilder()); $expression = $parser->parse($query); $filteredAstronauts = array_filter($astronauts, $expression); // array contains only 'Raccoons' astronauts // $filteredAstronauts = [ // ['name' => 'Thibaud', 'planet' => 'Raccoons of Asgard', 'points' => 760, 'rank' => 'Fleet Captain'], // ['name' => 'Mehdy', 'planet' => 'Raccoons of Asgard', 'points' => 675, 'rank' => 'Captain'], // ['name' => 'Romain', 'planet' => 'Raccoons of Asgard', 'points' => 550, 'rank' => 'Captain'], // ];
Tips: here we use the
$expression
witharray_filter
.
Now, I'd like to select astronauts with more than 1000 points but Raccoons
too.
<?php use Symftony\Xpression\Expr\ClosureExpressionBuilder; use Symftony\Xpression\Parser; // dataset $astronauts = [...]; $query = 'planet{{Raccoons}}|points≥1000'; $parser = new Parser(new ClosureExpressionBuilder()); $expression = $parser->parse($query); $filteredAstronauts = array_filter($astronauts, $expression); // contains only the 1000 points astronauts and the 'Raccoons' // $filteredAstronauts = [ // ['name' => 'Jonathan', 'planet' => 'Duck Invaders', 'points' => 5505, 'rank' => 'Fleet Admiral'], // ['name' => 'Thierry', 'planet' => 'Duck Invaders', 'points' => 2555, 'rank'=> 'Vice Admiral'], // ['name' => 'Vincent', 'planet' => 'Donut Factory', 'points' => 1885, 'rank' => 'Rear Admiral'], // ['name' => 'Rémy', 'planet' => 'Schizo Cats', 'points' => 1810, 'rank' => 'Rear Admiral'], // ['name' => 'Charles Eric', 'planet' => 'Donut Factory', 'points' => 1385, 'rank' => 'Commodore'], // ['name' => 'Ilan', 'planet' => 'Donut Factory', 'points' => 1325, 'rank' => 'Commodore'], // ['name' => 'Alexandre', 'planet' => 'Schizo Cats', 'points' => 1135, 'rank' => 'Commodore'], // ['name' => 'Thibaud', 'planet' => 'Raccoons of Asgard', 'points' => 760, 'rank' => 'Fleet Captain'], // ['name' => 'Mehdy', 'planet' => 'Raccoons of Asgard', 'points' => 675, 'rank' => 'Captain'], // ['name' => 'Romain', 'planet' => 'Raccoons of Asgard', 'points' => 550, 'rank' => 'Captain'], // ];
To filter an ArrayCollection we use the bridge Symftony\Xpression\Bridge\Doctrine\Common\ExpressionBuilderAdapter
.
<?php use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\ExpressionBuilder; use Symftony\Xpression\Bridge\Doctrine\Common\ExpressionBuilderAdapter; use Symftony\Xpression\Parser; // dataset $astronauts = [...]; // we wrap the array dataset in `ArrayCollection` $astronauts = new ArrayCollection($astronauts); $parser = new Parser(new ExpressionBuilderAdapter(new ExpressionBuilder())); $expression = $parser->parse($query); $filteredAstronauts = $astronauts->matching(new Criteria($expression));
ℹ️
ArrayCollection
are used by doctrine to manage relations (oneToMany, manyToMany etc...).
To filter
Collection
you can useClosureExpressionBuilder
and inject it inCollection::filter(Closure $p)
.
Now, we will filter some data in the MongoDB database.
Brace yourselves, it's gonna be tough!
<?php use Doctrine\Common\EventManager; use Doctrine\MongoDB\Connection; use Doctrine\MongoDB\Database; use Symftony\Xpression\Bridge\Doctrine\MongoDb\ExprBuilder; use Symftony\Xpression\Expr\ClosureExpressionBuilder; use Symftony\Xpression\Parser; // init connection $connection = new Connection('mongodb://localhost'); $database = $connection->selectDatabase('eleven-labs'); $collection = $database->selectCollection('astronauts'); $query = 'planet{{Raccoons}}|points≥1000'; $parser = new Parser(new ExprBuilder()); $queryBuilder = $parser->parse($query); $astronauts = $collection->createQueryBuilder()->setQueryArray($queryBuilder->getQuery())->getQuery()->execute(); // $astronauts is a Doctrine\MongoDB\Cursor // iterator_to_array($astronauts) = [ // ['name' => 'Jonathan', 'planet' => 'Duck Invaders', 'points' => 5505, 'rank' => 'Fleet Admiral'], // ['name' => 'Thierry', 'planet' => 'Duck Invaders', 'points' => 2555, 'rank'=> 'Vice Admiral'], // ['name' => 'Vincent', 'planet' => 'Donut Factory', 'points' => 1885, 'rank' => 'Rear Admiral'], // ['name' => 'Rémy', 'planet' => 'Schizo Cats', 'points' => 1810, 'rank' => 'Rear Admiral'], // ['name' => 'Charles Eric', 'planet' => 'Donut Factory', 'points' => 1385, 'rank' => 'Commodore'], // ['name' => 'Ilan', 'planet' => 'Donut Factory', 'points' => 1325, 'rank' => 'Commodore'], // ['name' => 'Alexandre', 'planet' => 'Schizo Cats', 'points' => 1135, 'rank' => 'Commodore'], // ['name' => 'Thibaud', 'planet' => 'Raccoons of Asgard', 'points' => 760, 'rank' => 'Fleet Captain'], // ['name' => 'Mehdy', 'planet' => 'Raccoons of Asgard', 'points' => 675, 'rank' => 'Captain'], // ['name' => 'Romain', 'planet' => 'Raccoons of Asgard', 'points' => 550, 'rank' => 'Captain'], // ];
Pretty simple in the end!
And doctrine/orm then?
<?php use Doctrine\ORM\Tools\Setup; use Symftony\Xpression\Bridge\Doctrine\ORM\ExprAdapter; use Symftony\Xpression\Expr\MapperExpressionBuilder; use Symftony\Xpression\Parser; // in this example I will use the annotation reader for my schema $config = Setup::createAnnotationMetadataConfiguration(array(__DIR__ . "/Orm/Entity"), true, null, null, false); $entityManager = EntityManager::create(array( // database configuration 'driver' => 'pdo_sqlite', 'path' => __DIR__ . '/ORM/astronauts.sqlite', ), $config); $query = 'planet{{Raccoons}}|points≥1000'; // MapperExpressionBuilder use to dynamicly add `a` alias the the query field $parser = new Parser(new MapperExpressionBuilder(new ExprAdapter(new Expr()), ['*' => 'a.%s'])); $expression = $parser->parse($query); $qb = $entityManager->getRepository('Example\Orm\Entity\Product')->createQueryBuilder('a'); $astronauts = $qb->where($expression)->getQuery()->execute(); // $astronauts = [ // ['name' => 'Jonathan', 'planet' => 'Duck Invaders', 'points' => 5505, 'rank' => 'Fleet Admiral'], // ['name' => 'Thierry', 'planet' => 'Duck Invaders', 'points' => 2555, 'rank'=> 'Vice Admiral'], // ['name' => 'Vincent', 'planet' => 'Donut Factory', 'points' => 1885, 'rank' => 'Rear Admiral'], // ['name' => 'Rémy', 'planet' => 'Schizo Cats', 'points' => 1810, 'rank' => 'Rear Admiral'], // ['name' => 'Charles Eric', 'planet' => 'Donut Factory', 'points' => 1385, 'rank' => 'Commodore'], // ['name' => 'Ilan', 'planet' => 'Donut Factory', 'points' => 1325, 'rank' => 'Commodore'], // ['name' => 'Alexandre', 'planet' => 'Schizo Cats', 'points' => 1135, 'rank' => 'Commodore'], // ['name' => 'Thibaud', 'planet' => 'Raccoons of Asgard', 'points' => 760, 'rank' => 'Fleet Captain'], // ['name' => 'Mehdy', 'planet' => 'Raccoons of Asgard', 'points' => 675, 'rank' => 'Captain'], // ['name' => 'Romain', 'planet' => 'Raccoons of Asgard', 'points' => 550, 'rank' => 'Captain'], // ];
⚠️ When we create queryBuilder with ORM we have to specifiy an alias EntityRepository::createQueryBuilder($alias)
. That's why it can't identify query field planet
.
First solution is to write the full quallified path field in the query like a.planet{{Raccoons}}|a.points≥1000
. But some database informations leak in the query.
The second solution is to use MapperExpressionBuilder
. This class will decorate ExpressionBuilder
to dynamically add the alias when the query builder is configured.
In the following example we prefix all (*
) fields with a
.
$parser = new Parser( new MapperExpressionBuilder( new ExprAdapter(new Expr()), ['*' => 'a.%s'] ) );
Many solutions are available if you want to filter your API:
This is not the lightest one. It is not the best choice to only filter data.
http query params are not readable and can be very heavy for complex query.
Good news! If your API uses one of the previous data sources, you can filter your endpoint with Xpression
.
Keep in mind that Xpression is different than GraphQL
We are going to use Xpression Bundle.
Install it with composer require symftony/xpression-bundle
then add it in symfony (AppKernel.php or bundle.php).
You must activate querystring correction to fix the parsing of reserved char.
<?php // public/index.php or web/app.php (web/app_dev.php) // ... \Symftony\Xpression\QueryStringParser::correctServerQueryString(); // add this line right before Request creation $request = Request::createFromGlobals(); // ...
To use it you just need to add annotation @Xpression(expressionBuilder="odm")
over desired filter controller.
<?php
namespace App\Controller;
use App\Document\Astronaut;
use App\Repository\AstronautsRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symftony\XpressionBundle\Annotations\Xpression;
class AstronautController extends AbstractController
{
/**
* @Xpression(expressionBuilder="odm")
*
* @return JsonResponse
*/
public function list(AstronautsRepository $astronautsRepository, $query = null)
{
$qb = $astronautsRepository->createQueryBuilder();
if (null !== $query) {
$qb->setQueryArray($query->getQuery());
}
return $this->json($qb->getQuery()->execute());
}
}
You can configure the following options:
Now you can go to your endpoint url and add query
.
http://localhost/astronauts/list?query={planet{{Raccoons}}|points≥1000}
This presentation is now over! Don't hesitate to test this library and contribute (idea, bugs, features, documentation, etc).
To do list :
Author(s)
Anthony MOUTTE
_Développeur / Concepteur @ ElevenLabs_🚀 je suis très intéressé par la recherche et développement ainsi que les bonnes pratiques.
You wanna know more about something in particular?
Let's plan a meeting!
Our experts answer all your questions.
Contact usDiscover other content about the same topic
You work with Symfony, but the concept of dependency injection is a little blurry for you? Find out how to take advantage of the component reading this article.
Are you suffering from domain anemia? Let's look at what an anemic domain model is and how things can change.
How to deploy PHP applications to AWS Lambda with Bref