Magic Doctrine2 finders when field has underscore? - doctrine

I'm having problems using find*() magic methods of Doctrine2 when the field has an underscore in between.
$repository->findByName("Hello"); // Works
$repository->findByIsEnabled(true);
Entity 'Acme\SecurityBundle\Entity\Package' has no field 'isEnabled'.
You can therefore not call 'findByIsEnabled' on the entities'
repository.
This is the simple entity definition in YAML for replicating the error:
Acme\SecurityBundle\Entity\Package:
type: entity
repositoryClass: Acme\SecurityBundle\Repository\PackageRepository
table: security_package
id:
id:
type: integer
generator: { strategy: AUTO }
fields:
name:
type: string
length: 255
unique: true
is_enabled:
type: boolean

I recall having had the same problem and think I solved it by writing something like this :
$repository->findBy(array('is_enabled' => true));
Let's look at the code :
<?php
/**
* Adds support for magic finders.
*
* #return array|object The found entity/entities.
* #throws BadMethodCallException If the method called is an invalid find* method
* or no find* method at all and therefore an invalid
* method call.
*/
public function __call($method, $arguments)
{
if (substr($method, 0, 6) == 'findBy') {
$by = substr($method, 6, strlen($method));
$method = 'findBy';
} else if (substr($method, 0, 9) == 'findOneBy') {
$by = substr($method, 9, strlen($method));
$method = 'findOneBy';
} else {
throw new \BadMethodCallException(
"Undefined method '$method'. The method name must start with ".
"either findBy or findOneBy!"
);
}
if ( !isset($arguments[0])) {
// we dont even want to allow null at this point, because we cannot (yet) transform it into IS NULL.
throw ORMException::findByRequiresParameter($method.$by);
}
$fieldName = lcfirst(\Doctrine\Common\Util\Inflector::classify($by));
if ($this->_class->hasField($fieldName) || $this->_class->hasAssociation($fieldName)) {
return $this->$method(array($fieldName => $arguments[0]));
} else {
throw ORMException::invalidFindByCall($this->_entityName, $fieldName, $method.$by);
}
}
The key line is here:
$fieldName = lcfirst(\Doctrine\Common\Util\Inflector::classify($by));
Now let's have a look to classify :
<?php
/**
* Convert a word in to the format for a Doctrine class name. Converts 'table_name' to 'TableName'
*
* #param string $word Word to classify
* #return string $word Classified word
*/
public static function classify($word)
{
return str_replace(" ", "", ucwords(strtr($word, "_-", " ")));
}
It looks like you're supposed to write your fields "likeThis" if you want this to work.

Related

Generate whole SQL statement with binding value to use as a key for cache function - CakePHP 4

Problem Description
I want to cache the query results with the key as a whole SQL statement instead part of the SQL statement like the below example:
// Generate a key based on a simple checksum
// of the query's where clause
$query->cache(function ($q) {
return md5(serialize($q->clause('where')));
});
Above example taken from this link : https://book.cakephp.org/4/en/orm/query-builder.html#caching-loaded-results
What I have tried
I can get the full SQL without the binding value like this:
$query->sql()
And the binding values like this:
$bindings = $query->getValueBinder()->bindings();
Now I need to figure out how to combine the both. It would be best if there is a built in function in CakePHP which would just give me the SQL with the binding value.
I have found the solution to this. There is a private function in DebugKit named interpolate() which create the full SQL statement with the binding value.
As the function is private, you have to copy it and save it in your source code.
Here's the interpolate function :
/**
* Helper function used to replace query placeholders by the real
* params used to execute the query.
*
* #param string $sql The SQL statement
* #param array $bindings The Query bindings
* #return string
*/
private static function interpolate($sql, array $bindings)
{
$params = array_map(function ($binding) {
$p = $binding['value'];
if ($p === null) {
return 'NULL';
}
if (is_bool($p)) {
return $p ? '1' : '0';
}
if (is_string($p)) {
$replacements = [
'$' => '\\$',
'\\' => '\\\\\\\\',
"'" => "''",
];
$p = strtr($p, $replacements);
return "'$p'";
}
return $p;
}, $bindings);
$keys = [];
$limit = is_int(key($params)) ? 1 : -1;
foreach ($params as $key => $param) {
$keys[] = is_string($key) ? "/$key\b/" : '/[?]/';
}
return preg_replace($keys, $params, $sql, $limit);
}
}
And then call it and pass the SQL and the binding values like this to get the whole SQL statement with the binding values:
$sql = $query->sql();
$bindings = $query->getValueBinder()->bindings();
// to make the example easier, I have saved the interpolate function in controller
$properSqlStatement = $this->interpolate($sql, $bindings);
🎉 Yay !

Operation without entity

I've been looking for a solution for a while but none of the one I find really allows me to do what I want. I would just like to create routes that don't necessarily require an entity or id to be used. Can you help me the documentation is not clear to do this.
Thank you beforehand.
As you can read in the General Design Considerations, just make an ordinary PHP class (POPO). Give it an ApiResource annontation like this:
* #ApiResource(
* collectionOperations={
* "post"
* },
* itemOperations={}
* )
Make sure the folder your class is in is in the paths list in api/config/packages/api_platform.yaml. There usually is the following configuration:
api_platform:
mapping:
paths: ['%kernel.project_dir%/src/Entity']
You should add your path if your class is not in the Entity folder.
Api Platform will expect json to be posted and try to unserialize it into an instance of your class. Make a custom DataPersister to process the instance, for example if your class is App\ApiCommand\Doit:
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\ApiCommand\Doit;
use App\ApiResult\DoitResult;
final class DoitDataPersister implements ContextAwareDataPersisterInterface
{
public function supports($data, array $context = []): bool
{
return $data instanceof Doit;
}
public function persist($data, array $context = [])
{
// code to process $data
$result = new DoitResult();
$result->description = 'Hello world';
return $result;
}
public function remove($data, array $context = [])
{
// will not be called if you have no delete operation
}
}
If you need Doctrine, add:
public function __construct(ManagerRegistry $managerRegistry)
{
$this->managerRegistry = $managerRegistry;
}
See Injecting Extensions for how to use it.
Notice that the result returned by ::persist is not an instance of Doit. If you return a Doit api platform will try to serialize that as the result of your operation. But we have marked Doit as an ApiResource so (?) api platform looks for an item operation that can retrieve it, resulting in an error "No item route associated with the type App\ApiCommand\Doit". To avoid this you can return any object that Symfonies serializer can serialize that is not an ApiResource. In the example an instance of DoitResult. Alternatively you can return an instance of Symfony\Component\HttpFoundation\Response but then you have to take care of the serialization yourself.
The post operation should already work, but the swagger docs are made from metadata. To tell api platform that it should expect a DoitResult to be returned, change the #ApiResource annotation:
* collectionOperations={
* "post"={
* "output"=DoitResult::class
* }
* },
This will the add a new type for DoitResult to the swagger docs, but the descriptions are still wrong. You can correct them using a SwaggerDecorator. Here is one for a 201 post response:
namespace App\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class SwaggerDecorator implements NormalizerInterface
{
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
$this->decorated = $decorated;
}
public function normalize($object, string $format = null, array $context = [])
{
$summary = 'short explanation about DoitResult';
$docs = $this->decorated->normalize($object, $format, $context);
$docs['paths']['/doit']['post']['responses']['201']['description'] = 'Additional explanation about DoitResult';
$responseContent = $docs['paths']['/doit']['post']['responses']['201']['content'];
$this->setByRef($docs, $responseContent['application/ld+json']['schema']['properties']['hydra:member']['items']['$ref'],
'description', $summary);
$this->setByRef($docs, $responseContent['application/json']['schema']['items']['$ref'],
'description', $summary);
return $docs;
}
public function supportsNormalization($data, string $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
private function setByRef(&$docs, $ref, $key, $value)
{
$pieces = explode('/', substr($ref, 2));
$sub =& $docs;
foreach ($pieces as $piece) {
$sub =& $sub[$piece];
}
$sub[$key] = $value;
}
}
To configure the service add the following to api/config/services.yaml:
'App\Swagger\SwaggerDecorator':
decorates: 'api_platform.swagger.normalizer.api_gateway'
arguments: [ '#App\Swagger\SwaggerDecorator.inner' ]
autoconfigure: false
If your post operation is not actually creating something you may not like the 201 response. You can change that by specifying the response code in the #ApiResource annotation, for example:
* collectionOperations={
* "post"={
* "output"=DoitResult::class,
* "status"=200
* }
* },
You may want to adapt the SwaggerDecorator accordingly.
Creating a "get" collection operation is similar, but you need to make a DataProvider instead of a DataPersister. The chapter9-api branch of my tutorial contains an example of a SwaggerDecorator for a collection response.
Thanks you for answer. I had some information but not everything. I will try the weekend.

doctrine 2 where condition without comparison

I have written a custom function for the DQL:
<?php namespace Bundle\DQL\Functions;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\Parser;
/**
* "DATE_COMPARE" "(" ArithmeticPrimary "," ComparisonOperator "," ArithmeticPrimary ")"
*/
class DateCompareFunction extends FunctionNode
{
public $date1;
public $date2;
public $operator;
/**
* #override
* #param SqlWalker $sqlWalker
* #return string
* #throws \Doctrine\DBAL\DBALException
*/
public function getSql(SqlWalker $sqlWalker)
{
return sprintf(
'TRUNC(%s) %s TRUNC(%s)',
$this->date1->dispatch($sqlWalker),
$this->operator,
$this->date2->dispatch($sqlWalker)
);
}
/**
* #override
* #param Parser $parser
*/
public function parse(Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->date1 = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_COMMA);
$this->operator = $parser->ComparisonOperator();
$parser->match(Lexer::T_COMMA);
$this->date2 = $parser->ArithmeticPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}
And my where stmt looks like this:
$query = $em->createQueryBuilder()
->select('evt')
->from('Application\Model\Event', 'evt')
->where('evt.USR_ID in (:uid)')
->setParameter('uid', $usersId);
if (null !== $from) {
$query->andWhere('DATE_COMPARE(evt.DAY, >, TO_DATE(:from, \'yyyy-mm-dd\'))')
->setParameter('from', $from);
The problem is that Doctrine raise an exception for having a WHERE statement without comparison symbol:
object(Doctrine\ORM\Query\QueryException)[347]
protected 'message' => string '[Syntax Error] line 0, col 130: Error: Expected =, <, <=, <>, >, >=, !=, got ')'' (length=80)
private 'string' (Exception) => string '' (length=0)
protected 'code' => int 0
protected 'file' => string 'C:\Workspace\app\hcalendar\vendor\doctrine\orm\lib\Doctrine\ORM\Query\QueryException.php' (length=88)
protected 'line' => int 52
private 'trace' (Exception) =>
I have tried adding a stmt = TRUE, but the generated statement isn't understood by oracle, hwo can I do a where statement without any comparison symbol ? (just a true/false function return)
Why do you need this function? you can do the where condition without custom function, just write:
$query->andWhere('evt.day > :from')->setParameter('from', $from);
where the variable $from should be a DateTime object, and if you want the Oracle TRUNC function you can implement it by it self as in here https://github.com/ZeinEddin/ZeDoctrineExtensions/blob/master/lib/ZeDoctrineExtensions/Query/Oracle/TruncDate.php and just use it like this:
$query->andWhere('trunc(evt.day) > :from')->setParameter('from', $from);
If you want you can install this module for a ZF2 project and you will have the TruncDate function ready to be used in your project

FOS Elastica -- getting string representation of query

I am unit-testing a repository that uses FOS Elastica and I was wondering if anyone knows how to get the string version of a query, rather than in array form. Here is my repository method:
/**
* Creates query object by either first or last name, with given parameters
*
* #param $name
*
* #param array $params
*
* #return Query
*/
public function findByFirstOrLast($name, array $params)
{
$queryString = new QueryString();
$queryString->setQuery($name);
$queryString->setFields(array('firstName', 'lastName'));
$query = new Query();
$query->setQuery($queryString);
$query->setSort(array($params['sort'] => array('order' => $params['direction'])));
return $query;
}
Assuming $name = 'foo'; (and that I am sorting on id), I believe the corresponding FOS Elastica query should be
{
"query":
{
"query_string":
{
"query":
"foo",
"fields":["firstName","lastName"]
}
},
"sort":
{
"id":
{
"order":"asc"
}
}
}
Does anyone know how to get this json-string representation of the query? It doesn't necessarily have to be in this pretty format either, it can be a one-line string.
I see you no longer are using this but I ended up needing the same thing.
Right before return $query you can use json_encode($query->getQuery()->toArray()) and that should give you what you need as a single line string.
Not a direct answer to the question but very related. When using a tool like found.no to test your elasticsearch queries, it can be interesting to have the output as YAML so you can paste in the found.no editor like this:
query:
filtered:
query:
multi_match:
query: php
operator: AND
fields:
- field1^30
- field2
- field3
- _all
You can have this kind of output with the following function:
use Elastica\Query;
use Symfony\Component\Yaml\Dumper;
/**
* #param Query $query
* #param bool $asYaml
*/
protected function debugQuery(Query $query, $asYaml = false)
{
echo '<pre>';
$debug = ['query' => $query->getQuery()->toArray()];
if (false === $asYaml) {
echo json_encode($debug, JSON_PRETTY_PRINT);
die();
}
$dumper = new Dumper();
$yaml = $dumper->dump($debug, 100);
echo $yaml;
die();
}
So you can choose either format.

How to create a Symfony2.1 Validation Constraint to validate the number of tokens in a string

Sometimes happen that a collection of values is stored in a database as a unique string. This string is so made of all the values separated by a user-defined delimiter (e.g. "," or "_").
What would be nice in a Symfony2.1 application is to have a Validation Constraint that validates a string (e.g. provided by an input text form) by counting the number of tokens included in that string.
A possible example is when you store the tags in a string format, i.e. you receive a string from an input field like value1,value2,value10,value25. You see that 4 tokens are passed, but there is no form validator that does that control for you. So, one should use such a validator like:
/**
* #Assert\Token(
* delimiter=",",
* min = "1",
* max = "5",
* minMessage = "You must specify at least one token",
* maxMessage = "You cannot specify more than 5 tokens")
*/
$tags;
There is something similar when using the new in Symfony2.1 Count validator, but is doesn't work on strings, just on array of objects that implements Countable.
Who know how to implement that kind of "tokenized string" validator?
I solved my problem, I just want to share my solutions.
One possible solution is to use a Callback constraint. For instance, following the tag list example provided in the question:
/**
* #Assert\Callback(methods={"isTagStringValid"})
*/
class AFormModel{
protected $tags;
public function isTagStringValid(ExecutionContext $context){
$tagsExploded = explode(',', $this->tags);
if(count($tagsExploded)==0){
$context->addViolationAtSubPath('tags', 'Insert at least a tag', array(), null);
}
if(count($tagsExploded)==1 && $tagsExploded[0]==='')
$context->addViolationAtSubPath('tags', 'Insert at least a tag', array(), null);
}
else if(count($tagsExploded)>10){
$context->addViolationAtSubPath('tags', 'Max 10 values', array(), null);
}
}
}
A more elegant way is to define the "Token" validator. An example follows here:
namespace .....
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class Token extends Constraint {
public $min;
public $max;
public $minMessage = '{{ min }} token(s) are expected';
public $maxMessage = '{{ max }} token(s) are expected';
public $invalidMessage = 'This value should be a string.';
public $delimiter = ',';
public function __construct($options = null){
parent::__construct($options);
if (null === $this->min && null === $this->max) {
throw new MissingOptionsException('Either option "min" or "max" must be given for constraint ' . __CLASS__, array('min', 'max'));
}
}
}
And the validator class is:
namespace ...
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class TokenValidator extends ConstraintValidator {
public function isValid($value, Constraint $constraint) {
if ($value === null) {
return;
}
if(!is_string($value)){
$this->context->addViolation($constraint->invalidMessage, array(
'{{ value }}' => $value,
));
return;
}
$tokensExploded = explode($constraint->delimiter, $value);
$tokens = count($tokensExploded);
if($tokens==1){
if($tokensExploded[0]==='')
$tokens = 0;
}
if (null !== $constraint->max && $tokens > $constraint->max) {
$this->context->addViolation($constraint->maxMessage, array(
'{{ value }}' => $value,
'{{ limit }}' => $constraint->max,
));
return;
}
if (null !== $constraint->min && $tokens < $constraint->min) {
$this->context->addViolation($constraint->minMessage, array(
'{{ value }}' => $value,
'{{ limit }}' => $constraint->min,
));
}
}
}
In this way you can import the user-defined validator and use it everywhere like I proposed in my question.

Resources