symfony2: how to use group_concat in QueryBuilder - symfony

I am having nested-set (using Gedmo tree) entity called "Location". Entity "Appartment" has location_id and what I need to do it to map scalar value called eg "path" to query that returns all appartments.
In Doctrine1, I had this code:
/**
* Add "path" to each element
*
* #param Doctrine_Query $query
* #param string $separator
*/
protected function addScalar_path(Doctrine_Query $query, $separator=", ")
{
$subquery = "k99.root_id=o.root_id AND k99.lft<=o.lft AND k99.rgt>=o.rgt AND k99.level<=o.level" ;
$query->addSelect("(SELECT GROUP_CONCAT(k99.name ORDER BY k99.level SEPARATOR '$separator') FROM Location k99 WHERE $subquery) AS path") ;
}
Note: "o" alias is used for primary query.
This code would allow me to use
{foreach .... as $appartment}
{$appartment->path}
...
Which would print:
Australia, Victoria, Melbourne, ...other children...
How to do the same thing in D2? And how to even include doctrine extenstions in my symfony2 project?

If you want to use it in QueryBuilder you must :
1) add DQL functions GroupConcat: GroupConcat
2 ) Registering GroupConcat :DQL User Defined Functions
another alternative is to use NativeQuery :Native SQL
In symfony2 registering a function DQL it's very simple just add GROUP_CONCAT in config.yml like:
entity_managers:
default:
dql:
string_functions:
GROUP_CONCAT: YourBundle\Query\Mysql\GroupConcat
For more information visit Registering Custom DQL Functions

If any comes across this post, there's now a Doctrine Extensions repository in Github. The repo has instructions on how to use it. You can use composer to install it and then use any function you're interested in.
EDIT:
For the Sake of the user Tushar, the way to use GROUP_CONCAT in Doctrine2's DQL is simple install Doctrine Extensions:
composer require beberlei/DoctrineExtensions
To enable it: add the following in your config.yml:
doctrine:
orm:
dql:
string_functions:
group_concat: DoctrineExtensions\Query\Mysql\GroupConcat
Then in your code, you should be able now to use Group Concat in your DQLs:
$this->createQueryBuilder('location')
->select('location')
->addSelect("GROUP_CONCAT(DISTINCT location.name SEPARATOR '; ') AS locationNames");
$result = $queryBuilder->getQuery()->getResult();
Or in the case for the original question:
$query->addSelect("(SELECT GROUP_CONCAT(k99.name ORDER BY k99.level SEPARATOR '$separator') FROM Location k99 WHERE $subquery) AS path");

Just an addition to #a.aitboudad's answer: The DQL function GroupConcat linked appears to have a «limited support for GROUP_CONCAT».
Here is the full support version :
The configuration is as he mentioned.
// -------------------------------------------------
// Complete support of GROUP_CONCAT in Doctrine2
// -------------------------------------------------
// Original Article: http://habrahabr.ru/post/181666/
// Automated translation to English: http://sysmagazine.com/posts/181666/
// Original github commit: https://github.com/denisvmedia/DoctrineExtensions/blob/d1caf21cd7c71cc557e60c26e9bf25323a194dd1/lib/DoctrineExtensions/Query/Mysql/GroupConcat.php
/**
* DoctrineExtensions Mysql Function Pack
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to kontakt#beberlei.de so I can send you a copy immediately.
*/
namespace DoctrineExtensions\Query\Mysql;
use Doctrine\ORM\Query\AST\Functions\FunctionNode,
Doctrine\ORM\Query\Lexer;
/**
* Full support for:
*
* GROUP_CONCAT([DISTINCT] expr [,expr ...]
* [ORDER BY {unsigned_integer | col_name | expr}
* [ASC | DESC] [,col_name ...]]
* [SEPARATOR str_val])
*
*/
class GroupConcat extends FunctionNode
{
public $isDistinct = false;
public $pathExp = null;
public $separator = null;
public $orderBy = null;
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$lexer = $parser->getLexer();
if ($lexer->isNextToken(Lexer::T_DISTINCT)) {
$parser->match(Lexer::T_DISTINCT);
$this->isDistinct = true;
}
// first Path Expression is mandatory
$this->pathExp = array();
$this->pathExp[] = $parser->SingleValuedPathExpression();
while ($lexer->isNextToken(Lexer::T_COMMA)) {
$parser->match(Lexer::T_COMMA);
$this->pathExp[] = $parser->StringPrimary();
}
if ($lexer->isNextToken(Lexer::T_ORDER)) {
$this->orderBy = $parser->OrderByClause();
}
if ($lexer->isNextToken(Lexer::T_IDENTIFIER)) {
if (strtolower($lexer->lookahead['value']) !== 'separator') {
$parser->syntaxError('separator');
}
$parser->match(Lexer::T_IDENTIFIER);
$this->separator = $parser->StringPrimary();
}
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
$result = 'GROUP_CONCAT(' . ($this->isDistinct ? 'DISTINCT ' : '');
$fields = array();
foreach ($this->pathExp as $pathExp) {
$fields[] = $pathExp->dispatch($sqlWalker);
}
$result .= sprintf('%s', implode(', ', $fields));
if ($this->orderBy) {
$result .= ' ' . $sqlWalker->walkOrderByClause($this->orderBy);
}
if ($this->separator) {
$result .= ' SEPARATOR ' . $sqlWalker->walkStringPrimary($this->separator);
}
$result .= ')';
return $result;
}
}
// -------------------------------------------------
// Example of usage:
// -------------------------------------------------
$query = $this->createQueryBuilder('c')
->select("
c as company,
GroupConcat(b.id, ';', b.headOffice, ';', b.city, ';', s.name
ORDER by b.id
SEPARATOR '|') AS branches
")->leftJoin('c.branches', 'b')
->leftJoin('b.country', 's')
->groupBy('c.id')
->setFirstResult(0)
->setMaxResults(10)
->getQuery();
$result = $query->getResult();

I had similar problem. GROUP_CONCAT does not allow using CASE-WHEN inside.
This library fixed my issues, so maybe it will be helpful.

Related

Drupal 8 - Get All Nodes With a Part of Url Alias

I try to find a way to get all nodes by a part of the url alias in the nodes. I know i get a specific node by the complete url alias. For example:
$path = \Drupal::service('path.alias_manager')->getPathByAlias('/this-is-the-alias');
if(preg_match('/node\/(\d+)/', $path, $matches)) {
$node = \Drupal\node\Entity\Node::load($matches[1]);
}
And I know I can find multiple nodes by entity query:
$nids = \Drupal::entityQuery('node')
->condition('type','page')
->condition('status',1)
->sort('created', 'DESC')
->range(0, 20)
->execute();
But I want to know how I can implement an additional condition to filter nodes by a part of the URL alias for example "/news/*". I need all nodes with this url fragment.
Can try this-
Reference https://www.jonkamke.com/blog/2019/5/load-a-node-via-url-alias
<?php
/** #var Drupal\Core\Url $url */
$url = $variables['url'];
if ($url instanceof Drupal\Core\Url) {
$nid = $url->getRouteParameters()['node'];
/** #var \Drupal\node\Entity\Node $node */
$node = Node::load($nid);
if ($node instanceof Node) {
$type = $node->getType();
}
}
$input = '/this-is-the-alias';
$node_query_result = [];
$path_query = \Drupal::database()->select('path_alias', 'a');
$path_query->addField('a', 'path');
$path_query->condition('a.alias', '%' . $input . '%', 'LIKE');
$path_query_result = str_replace('/node/', '', $path_query->execute()->fetchCol());
if ($path_query_result) {
$node_query = \Drupal::database()->select('node_field_data', 'n');
$node_query->addField('n', 'nid');
$node_query->addField('n', 'type');
$node_query->addField('n', 'title');
$node_query->condition('n.status', NodeInterface::PUBLISHED);
$node_query->condition('nid', $path_query_result, 'IN');
$node_query_result = $node_query->execute()->fetchAll(\PDO::FETCH_ASSOC);
}
return $node_query_result;

Gedmo Translatable persist default translation

I have a website built with Symfony 2.8 and Gedmo Translatable.
In order to use HINT_INNER_JOIN and filter items which don't have a translation I had to set persist_default_translation to true:
stof_doctrine_extensions:
default_locale: '%locale%' # TODO: what does it happen when removing this line?
translation_fallback: true
persist_default_translation: true
orm:
default:
timestampable: true
blameable: true
translatable: true
Unfortunately this caused that my existing translations for the default language are no more persisted (and they appear empty).
I would need to force re-save all of my entities to generate the default locale again.
How can I do that? I tried with clone and persist but it creates a duplicate of the entity.
Is it possible to force Doctrine to update all the fields again?
I ended up creating a custom command to migrate all the translations. I created a fake translation called "kr" and then updated all the record with "kr" to "fr" with an SQL query.
I used reflection and other "black magic" to get the properties with the Translatable annotation, maybe this could help someone with the same problem. Here is the code:
class NormalizeTranslationsCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
// the name of the command (the part after "app/console")
->setName('app:normalize-translations')
// the short description shown while running "php app/console list"
->setDescription('Normalizes the translations.')
// the full command description shown when running the command with
// the "--help" option
->setHelp('This command allows you to normalize the translations...')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
// all the translatable classes
$classes = [
Entities\MyClass1::class,
Entities\MyClass2::class,
];
foreach ($classes as $class) {
$this->processClass($class, $output);
}
}
private function processClass($class, $output)
{
$output->writeln(sprintf('Processing class <info>%s</info>', $class));
// gets all the properties
$properties = $this->getProperties($class);
// gets the translatable properties
$translatableProperties = $this->getTranslatableProperties($properties, $class);
$output->writeln(sprintf('Found %d translatable properties: %s', count($translatableProperties), implode(', ', $translatableProperties)));
$defaultLanguage = 'kr'; // fake language
$em = $this->getContainer()->get('doctrine')->getManager();
$repository = $em->getRepository('Gedmo\\Translatable\\Entity\\Translation');
$items = $em->getRepository($class)->findAll();
$propertyAccessor = PropertyAccess::createPropertyAccessor();
foreach ($items as $item) {
foreach ($translatableProperties as $translatableProperty) {
$value = $propertyAccessor->getValue($item, $translatableProperty);
$repository->translate($item, $translatableProperty, $defaultLanguage, $value);
}
$em->flush();
}
}
private function getProperties($class)
{
$phpDocExtractor = new PhpDocExtractor();
$reflectionExtractor = new ReflectionExtractor();
// array of PropertyListExtractorInterface
$listExtractors = array($reflectionExtractor);
// array of PropertyTypeExtractorInterface
$typeExtractors = array($phpDocExtractor, $reflectionExtractor);
// array of PropertyDescriptionExtractorInterface
$descriptionExtractors = array($phpDocExtractor);
// array of PropertyAccessExtractorInterface
$accessExtractors = array($reflectionExtractor);
$propertyInfo = new PropertyInfoExtractor(
$listExtractors,
$typeExtractors,
$descriptionExtractors,
$accessExtractors
);
return $propertyInfo->getProperties($class);
}
private function getTranslatableProperties($properties, $class)
{
$translatableProperties = [];
// https://gist.github.com/Swop/5990316
$annotationReader = new AnnotationReader();
foreach ($properties as $property) {
try {
$reflectionProperty = new \ReflectionProperty($class, $property);
$propertyAnnotations = $annotationReader->getPropertyAnnotations($reflectionProperty);
foreach ($propertyAnnotations as $propertyAnnotation) {
if ($propertyAnnotation instanceof Translatable) {
// this property is translatable
$translatableProperties[] = $property;
}
}
} catch (\ReflectionException $e) {
// missing property
continue;
}
}
return $translatableProperties;
}
}

Slim log writer with sqlite

I have configured slim to write logs to log files as the standard way. But this is not effective when we want to search large and all the logs at a given time. So I want to write those logs to a separate sqlite DB.
My question is how can I set the log writer to write the messages (as done in the Zend framework) ?
P S: I know that I can create a PDO object and use the queries. But I don't want to change the existing code. Just prefer to set the writer and let the framework do the job for me.
I managed to do this as follows,
Create the sqlite connection
$sqlite = new PDO('sqlite:./logs/log.db');
Create my own LogWritter similar to the framework
<?php
/**
* Description of LogWritter
*
* #author Ruwantha.Lankathilaka
*/
class LogWritter {
protected $sqliteConnection;
public function __construct($connection) {
$this->sqliteConnection = $connection;
}
/**
* Write function will bypass the slim default LogWriter and will return
* last inserted log id which could be used as a reference
*
* #param type $object will get the error message
* #param type $level will get the error levels of \Slim\Log
* #return mix if successfully logged will return the last insert id, else
* will return false
*/
public function write($object,$level) {
//Determine label
$label = 'DEBUG';
$message = (string) $object;
switch ($level) {
case \Slim\Log::FATAL:
$label = 'FATAL';
break;
case \Slim\Log::ERROR:
$label = 'ERROR';
break;
case \Slim\Log::WARN:
$label = 'WARN';
break;
case \Slim\Log::INFO:
$label = 'INFO';
break;
}
$sqliteQuery = "INSERT INTO logs (lable,message) VALUES (:lable,:message)";
$statement = $this->sqliteConnection->prepare($sqliteQuery);
$result = $statement->execute(array(':lable'=>$label,':message'=>$message));
if(!empty($result)){
return $this->sqliteConnection->lastInsertId();
}else{
return false;
}
}
}
Add the LogWritter to the index
Add the LogWritter to the Slim app
$app = new \Slim\Slim(array(
'log.writer' => $logWriter,
'log.enabled' => true,
'log.level' => \Slim\Log::DEBUG,
'debug' => true
));
now you can get the log from app
$retult = $app->log->error('test error');
$result will have the inserted log id false if the log failed
Hope this will help someone in future.

how to access annotation of an property(class,mappedBy,inversedBy)

Good morning,
Is it exist an function where I pass an entity and the propertyName and return me the mappedBy,inversedBy and absoluteClassName of an Entity.
The goal is to use the __call to create automatic getteur/setteur and addFucntion bidirectionnal.
I don't want to use generates Entities I want all getteur,setteur and add Function use __call.
But i can"t do an addBirectionnal if i don't know if the relation is many to many or one to many and if i don't know the name of the mappedBy.
my code:
public function __get($p){
return $this->$p;
}
public function __set($p,$v){
$this->$p = $v;
return $this;
}
public function __call($name,$arguments){
if(substr($name,1,3)=='et')
$name2 = substr(3);
if($name[0] == 'g'){
return $this->$name2;
}else{//substr($name,0,1) == 's'
$this->$name2 = $arguments[0];
/*for a one to one*/
/*$mappedByName= getmappedByOrInversedBy(get_class($name),$name2);
if($mappedByName){
$this->$name->$mappedByName = $this;/
}*/
return $this;
}
}
}
I need getmappedByOrInversedBy, thanks.
edit: I try this
public function test(){
$str = "AppBundle\Entity\Group";
$mapping = new \Doctrine\ORM\Mapping\ClassMetadataInfo($str);
$d = $mapping->getAssociationMappedByTargetField('trad');
var_dump($d);
return $this->render('default/index.html.twig', array(
'base_dir' => realpath($this->getParameter('kernel.root_dir').'/..'),
));
}
class Group
{
...
/**
* #ORM\OneToOne(targetEntity="Traduction",inversedBy="grp")
*/
protected $trad;
}
Result : Undefined index: trad
The ClassMetadataInfo is what you are looking for.
Creates an instance with the entityName :
$mapping = new \Doctrine\ORM\Mapping\ClassMetadataInfo($entityNamespaceOrAlias);
Then, get the informations you want :
Get all association names: $mapping->getAssociationNames();
Get the join column of an association:
$mapping->getSingleAssociationJoinColumnName($fieldName);
Get the mappedBy column of an association:
$mapping->getAssociationMappedByTargetField($fieldName);
...
Look at the class to know which method you can access.
Hopes it's what you expect.
EDIT
As you can access the EntityManager (i.e. from a controller), use :
$em = $this->getDoctrine()->getManager();
$metadata = $em->getClassMetadata('AppBundle:Group');
To be sure there is no problem with your entity namespace, try :
print $metadata->getTableName();
To retrieve the associations of the entity, use :
$metadata->getAssociationNames();
And to get the mapping informations of an existing association, use :
$metadata->getAssociationMapping($fieldName);
And to get all the association mappings of your entity, use:
$metadata->getAssociationMappings();

Laravel Eloquent hasMany returns null

Laravel 4 is giving me unexpected results on a new project. To try and help me better understand the results I tried what I thought would be a simple exercise and use a friends old WordPress blog on his web hosting.
Post model:
class Post extends Eloquent {
protected $table = 'wp_posts';
public function meta()
{
return $this->hasMany('Meta', 'post_id');
}
}
Meta model
class Meta extends Eloquent {
protected $table = 'wp_postmeta';
public function post()
{
return $this->belongsTo('Post');
}
}
I have tried all of these variations with no avail...
TestController:
public function get_index()
{
// $test = Post::with('Meta')->get();
// $test = Post::with('Meta')->where('id', '=', '219')->first();
// $test = Post::find(219)->Meta()->where('post_id', '=', '219')->first();
// $test = Post::find($id)->Meta()->get();
// $test = Meta::with('Post')->get();
$id = 219;
$test = Post::find($id)->meta;
return $test;
}
returned in the event listener:
string(47) "select * from `wp_posts` where `id` = ? limit 1" string(65) "select * from `wp_postmeta` where `wp_postmeta`.`post_id` is null" []
Please tell me I am just overlooking something really minor and stupid and I just need some sleep.
Though is that while the SQL is case-insensitive, the array of attributes the model gets populated with is a PHP array where indexes are case-sensitive. The schema had the primary key as ID

Resources