FOS Elastica -- getting string representation of query - elasticsearch

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.

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.

Laravel Nova Metric Trend In Future

I'm stuck I want to create a Laravel Nova Metrics Trend where it goes in the future instead of back in time.
This code will output: 15-March-2019, 14-March-2019, 13-March-2019,
public function calculate(Request $request)
{
return $this->sumByDays($request, Payment::class, 'hours_per_month', 'upcoming_invoice_date')->showLatestValue();
}
/**
* Get the ranges available for the metric.
*
* #return array
*/
public function ranges()
{
return [
3 => '3 Days',
];
}
I've already made some changes to Trend.php but it gave me nothing than errors.
/**
* Return a value result showing a sum aggregate over days.
*
* #param \Illuminate\Http\Request $request
* #param \Illuminate\Database\Eloquent\Builder|string $model
* #param string $column
* #param string $dateColumn
* #return \Laravel\Nova\Metrics\TrendResult
*/
public function sumByDays($request, $model, $column, $dateColumn = null)
{
return $this->aggregate($request, $model, Trend::BY_DAYS, 'sum', $column, $dateColumn);
}
Is this even possible?
Thanks,
The trend range is defined in Trend.php so you were on the right track!
Check out the protected function aggregate.
protected function aggregate($request, $model, $unit, $function, $column, $dateColumn = null)
{
The $endingDate can be changed to whatever you want!
$possibleDateResults = $this->getAllPossibleDateResults(
$startingDate = $this->getAggregateStartingDate($request, $unit),
$endingDate = Chronos::now();
The Chronos API has nice documentation but try something like this
$endingDate = Chronos::tomorrow()->addWeeks(4),
Note: I have done very minimal testing on this so use at your own risk. It does break the showLatestValue() method for trend metrics, but that could be addressed in TrendResult.php if you really needed it.
1) You can change the range dates to the future with a negative number (of days) as key:
public function ranges()
{
return [
-3 => 'Next 3 Days',
];
}
The problem is that it doesn't work because the order of datetimes will be wrong in the query as the endDate is always set to now() in Trend.php:
$startingDate = $this->getAggregateStartingDate($request, $unit),
$endingDate = Chronos::now(),
...
->whereBetween($dateColumn, [$startingDate, $endingDate])
2) Quick/dirty fix. In Trend.php change the whereBetween() second argument array (of datetimes) comparing and setting datetimes in ascending order.
->whereBetween(
$dateColumn,
[ min($startingDate, $endingDate), max($startingDate, $endingDate) ]
)

Eloquent: querying using a non-existing column

I'm trying the following: I have two models (Pub and Schedule) related by a 1xN relationship as follows:
Pub:
/**
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function pubSchedules()
{
return $this->hasMany(Schedule::class);
}
Schedule:
/**
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function pub()
{
return $this->belongsTo(Pub::class);
}
Table schedules has the following fields:
id | pub_id | week_day | opening_time | closing_time |
I use the following function to know if one pub is currently (or not) open:
/**
* #return bool
*/
public function isPubCurrentlyOpen()
{
$schedules = Schedule::where([
['pub_id', $this->id ],
['week_day', Carbon::now()->dayOfWeek],
])->get();
foreach ($schedules as $schedule){
$isOpen[] =
Carbon::now('Europe/Madrid')->between(
Carbon::now('Europe/Madrid')->setTimeFromTimeString($schedule->opening_time),
Carbon::now('Europe/Madrid')->setTimeFromTimeString($schedule->closing_time)
);
}
if(in_array(true, $isOpen)){
return true;
//return "Pub Opened";
}
return false;
//return "Pub Closed";
}
In my PubController I'd like, when the option "Filter by open pubs" is chosen if($request->openPubs == 1), to show only opened pubs isOpen ==true.
Knowing the relationships between models, how can I do it?
I'm looking for something like this:
if($request->openPubs == 1)
{
$pubs = $pubs->with('pubSchedules')->where('isOpen' == true);
}
Can you help me?
Thanks a lot!
You can do this using a "whereHas"
$openPubs = Pub::whereHas('schedule', function ($query) {
$query->where('week_day', Carbon::now()->dayOfWeek);
$query->whereRaw(
"'".Carbon::now('Europe/Madrid')->format("H:i:s")."' BETWEEN opening_time AND closing_time"
);
})->get();
This is assuming your opening time and closing time are the appropriate time format and not strings (though strings will work as well in a 24h format).
You might achieve something similar to what you are looking for by using a scope e.g.
public function scopeFilterBy($query, $filter = null) {
if ($filter == "isOpen") {
$query->whereHas('schedule', function ($query) {
$query->where('week_day', Carbon::now()->dayOfWeek);
$query->whereRaw(
"'".Carbon::now('Europe/Madrid')->format("H:i:s")."' BETWEEN opening_time AND closing_time"
);
});
}
return $query; //Not sure if this is needed
}
You could then do:
Pub::filterBy($request->openPubs ? "isOpen" : null)->get();
I don't fully understand how you are trying to accomplish this but it should be something like this
$pubs = Pub::with(['pubSchedules' => function ($query) {
$query->where('opening_time', '>' ,Carbon::now()) // make sure it's currently open
->where('closing_time', '<' ,Carbon::now()) // make sure that it's not finished already
->where('week_day', '==' ,Carbon::now()->dayOfWeek) // make sure it's today
}])->find($id);
// to get if pub is currently
if($pub->pubSchedules->count()){
//
}
you can put this code in the model (Pub) and make some changes
if you already have the object you can do this (Add it to model)
public function isPubOpen()
{
$this->load(['pubSchedules' =>
// same code in other method
]);
return (bool) $this->pubSchedules->count();
}
For small tables you could call the function isPubCurrentlyOpen for each element.
For this you would need to change your function to recieve the pub_id as a parameter:
public function isPubCurrentlyOpen($pub_id)
{
$schedules = Schedule::where([
['pub_id', $pub_id ],
['week_day', Carbon::now()->dayOfWeek],
])->get();
foreach ($schedules as $schedule){
$isOpen[] =
Carbon::now('Europe/Madrid')->between(
Carbon::now('Europe/Madrid')->setTimeFromTimeString($schedule->opening_time),
Carbon::now('Europe/Madrid')->setTimeFromTimeString($schedule->closing_time)
);
}
if(in_array(true, $isOpen)){
return true;
//return "Pub Opened";
}
return false;
//return "Pub Closed";
}
and to query the data do:
if($request->openPubs == 1)
{
// assuming $pubs is a collection instance
$pubs = $pubs->filter(function($a){
return $this->isPubCurrentlyOpen($a->id);
})
}
There's a feature in Eloquent called Eager Loading. The Eloquent ORM provides a simple syntax to query for all the Schedules that are related with this particular Pub as described below:
$pubIsOpen= $pub->schedules()
->where([
['week_day', Carbon::now()->dayOfWeek],
['opening_time' , '<' , Carbon::now('Europe/Madrid')],
['closing_time' , '>' , Carbon::now('Europe/Madrid')]
])
->count();
if($openPubCount > 0){
//PUB is open
}else{
//PUB is closed
}
If it helps to someone in the future I post my solution, thanks to #apokryfos:
Pub:
/**
* #param $pubs
* #return mixed
*/
public static function isPubCurrentlyOpen($pubs)
{
$pubs->whereHas( 'pubSchedules', function ($pubs) {
$pubs->where( 'week_day', Carbon::now()->dayOfWeek )
->whereRaw(
"'" . Carbon::now( 'Europe/Madrid' )->format( "H:i:s" ) . "' BETWEEN opening_time AND closing_time"
);
} );
return $pubs;
}
PubsController:
/**
* #param GetPubRequest $request
* #return ApiResponse
*/
public function getPubs(GetPubRequest $request)
{
$orderBy = 'id';
$order = 'asc';
$pubs = Pub::withDistance();
............
if($request->openPubs == 1)
{
$pubs = Pub::isPubCurrentlyOpen($pubs);
}
return $this->response(PubProfileResource::collection($pubs->orderBy($orderBy, $order)->paginate()));
}

Magic Doctrine2 finders when field has underscore?

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.

Resources