I´m looking for a solution to make the andX part of a doctrine DQL statement dynamic according to passed arguments:
private function getDiscountPrice(int $weighting, Article $article, Customer $customer, array $args) {
// Get the query builder
$qb = Shopware()->Models()->createQueryBuilder();
$params = new ArrayCollection();
foreach($args as $key => $arg):
$params->set($key,$arg);
endforeach;
$qb->select('discount')
->from('PhaBase\Models\Discount','discount')
->where(
$qb->expr()->andX(
$qb->expr()->eq('discount.kdNr',':kdNr'),
$qb->expr()->eq('discount.pzn',':pzn'),
$qb->expr()->isNotNull('discount.kdNr'),
$qb->expr()->isNotNull('discount.pzn')
)
)
->setParameters($params->toArray());
$discount = null;
try {
$discount = $qb->getQuery()->getOneOrNullResult();
} catch (NonUniqueResultException $e) {
// #TODO Add log entry to inform about Exception
return null;
}
I want to build the content for the andX() argument in the same way like the Parameters, but I don´t know how to pass dynamic arguments to this method.
Thanks for any ideas,
Michael
Here´s the solution - just found it:
private function getDiscountPrice(int $weighting, Article $article, Customer $customer, array $args) {
// Get the query builder
$qb = Shopware()->Models()->createQueryBuilder();
$params = new ArrayCollection();
$conditions = new ArrayCollection();
foreach($args as $key => $arg):
$params->set($key,$arg);
$conditions->add($qb->expr()->eq('discount.'.$key,':'.$key));
endforeach;
$conditions = call_user_func_array(array($qb->expr(), 'andX'), $conditions->toArray());
$qb->select('discount')
->from('PhaBase\Models\Discount','discount')
->where($conditions)
->setParameters($params->toArray());
$discount = null;
try {
$discount = $qb->getQuery()->getOneOrNullResult();
} catch (NonUniqueResultException $e) {
// #TODO Add log entry to inform about Exception
return null;
}
// If a discount was found calculate the price for this discount and return it.
if (!is_null($discount)):
$discountPrice = $this->calculateDiscountPrice($article, $discount, $customer);
You can add() additional expressions directly to andX() or orX()
Instead of adding your expressions to an ArrayCollection you can add them directly to an andX expression (and only add that to the main query if there are actually expressions inside of it). Same goes for the required parameters.:
private function getDiscountPrice(int $weighting, Article $article, Customer $customer, array $args)
{
// Early out if there's nothing to do.
if (empty($args)) {
return null;
}
$qb = Shopware()->Models()->createQueryBuilder()
->select('discount')
->from(PhaBase\Models\Discount::class,'discount')
;
$conditions = $qb->expr()->andX();
// Instead of setting each parameter individually (see below) you could set them
// all at once without the need for an ArrayCollection(), like you used, because your
// $args array is already in the correct format.
// $qb->setParameters($args);
foreach($args as $key => $arg) {
$conditions->add('discount.'.$key.' = :'.$key);
$qb->setParameter($key, $arg);
}
// You could check for $conditions->count() > 0 here, but we already did that at the beginning.
try {
return $this->calculateDiscountPrice(
$article,
$qb->where($conditions)->getQuery()->getOneOrNullResult(),
$customer
);
} catch (NonUniqueResultException $e) {
// #TODO Add log entry to inform about Exception
return null;
}
}
A little late to the party but maybe it helps someone else.
Related
Hello i have 2 tables that i want to call right now, for the EDIT (part of the CRUD)
tables:
table_a
table_b
i found in youtube how to update/edit from 2 tables, i need to call bot of the tables.
here's the code for the model
public function edit_this($ID_A)
{
return $this->db->table('table_a', '*i don't know how to insert the 2nd table')->where('ID_A', $ID_A)->get()->getRowArray();
}
Here's the controller
public function this_edit($ID_A)
{
$data = [
'title' => 'Admin',
'navbartitel' => 'You know this',
'alledit' => $this->theModel->edit_this($ID_A),
'validation' => \Config\Services::validation()
];
return view('this/all/edit', $data);
}
it works but i only can accsess the tabel_a, but i need them both so i can show what i've written in the edit form, from the database
anyone can help? thank you
$this->db->table(...) returns an instance of QueryBuilder and will happily accept a single string of comma-separated tables ("table1, table2..."), or even an array for that matter (['table1', 'table2'...]), as its first parameter. You are doing neither and instead passing multiple parameters.
When you call table(), the value passed in the first parameter is used during the creation of the database-specific Builder class:
public function table($tableName)
{
if (empty($tableName))
{
throw new DatabaseException('You must set the database table to be used with your query.');
}
$className = str_replace('Connection', 'Builder', get_class($this));
return new $className($tableName, $this);
}
The DB-specific Builder class has no constructor of its own so falls back on the __construct defined in BaseBuilder, which it extends:
public function __construct($tableName, ConnectionInterface &$db, array $options = null)
{
if (empty($tableName))
{
throw new DatabaseException('A table must be specified when creating a new Query Builder.');
}
$this->db = $db;
$this->from($tableName);
...
I've truncated this for brevity because the important part is that call to $this->from, which is in the end how multiple tables get processed:
public function from($from, bool $overwrite = false)
{
if ($overwrite === true)
{
$this->QBFrom = [];
$this->db->setAliasedTables([]);
}
foreach ((array) $from as $val)
{
if (strpos($val, ',') !== false)
{
foreach (explode(',', $val) as $v)
{
$v = trim($v);
$this->trackAliases($v);
$this->QBFrom[] = $v = $this->db->protectIdentifiers($v, true, null, false);
}
}
else
{
$val = trim($val);
// Extract any aliases that might exist. We use this information
// in the protectIdentifiers to know whether to add a table prefix
$this->trackAliases($val);
$this->QBFrom[] = $this->db->protectIdentifiers($val, true, null, false);
}
}
return $this;
}
I've got quite a bizarre situation. I've got a piece of code that never gave any issues in the past. Since last night it behaves like this.
Before updating a model the id of that model goes to true. The function below is from a controller and gets called with a POST request. The request gets validated and when the model has not been exported it gets exported to another system. If the export is successful, the model gets updated with the appropriate values. The id does not get set in any stage of this process.
I've added comments to the code to give you an idea where, what happens.
public function export(Request $request, VeniceService $service, Invoice $invoice)
{
$invoice = $invoice->load([
'user', 'customer', 'extension.referenceValues.definition', 'lines'
]);
$this->enforce([
new CheckCstNum($invoice->customer),
new CheckReferences($invoice->extension),
], 432);
if ($invoice->to_export) {
DB::beginTransaction();
try {
var_dump($invoice->id); // returns the id
$data = $service->export($invoice);
var_dump($invoice->id); // returns the true
$invoice->book_date = Carbon::now();
$invoice->doc_num = $data['doc_num'];
$invoice->sys_num = $data['sys_num'];
$invoice->tsm_num = $data['tsm_num'];
$invoice->to_export = false;
$invoice->is_ticked = false;
var_dump($invoice->id); // This returns true
var_dump($invoice); // All the values are correct, except the id, this is set to true
$invoice->save(); // With the id as true, this throws an exception. Duplicate entries for PRIMARY key id, '1'
DB::commit();
$service->attachPdf($invoice, Printer::print($invoice)->output());
} catch (VeniceException $e) {
DB::rollBack();
return $e->render($request);
} catch (\Exception $e) {
DB::rollBack();
return response()->json($e->getMessage(), 500);
}
}
return new InvoiceResource($invoice->refresh()); // returns the invoice, but the id is still true
}
$this->service->export() resolves to this function. Before this happens, the id is still the original id of the model.
public function export($model)
{
return $this->call($model, __FUNCTION__);
}
protected function call($model, $function)
{
$class = $this->getClassName($model);
$method = "{$function}{$class}";
return $this->$method($model);
}
public function exportInvoice($invoice)
{
var_dump($invoice->id); // Returns the id
$veniceInvoice = (new VeniceInvoiceResource($invoice))->toArray(request());
var_dump($invoice->id); // Returns true...
return $this->request('POST', 'venice/invoices/' . $this->bookSales, [
RequestOptions::JSON => $veniceInvoice
]);
}
$veniceInvoice = (new VeniceInvoice($invoice))->toArray(request()); After this line the id is set as true. This really does not make any sense as it has always worked, and the model does not get manipulated in any way.
One last bit of code. But I do not think this has anything to do with the issue.
VeniceInvoiceResource.php
public function toArray($request)
{
$pdf = Printer::print($this->resource)->output();
$lines = $this->customer->standard_base == 10 ? VeniceInvoiceLineIC::collection($this->lines) : VeniceInvoiceLine::collection($this->lines);
$refs = $this->extension->referenceValues->map(function ($item) {
return [
'index' => 0,
'quantity' => 0,
'unit_price' => 0,
'description' => $item->definition->name . ' ' . $item->value,
'vat_code' => 0,
'ic_code' => 0,
];
})->toArray();
$details = array_merge($refs, $lines->toArray($request));
return [
'cst_num' => $this->customer->cst_num,
'book' => ($this->book === 'VKPCN') ? $this->book : config('venice.config.sales.book'),
'amount' => $this->total,
'vat_amount' => $this->total,
'exp_date' => carbon(config('venice.config.sales.date'))->addDays($this->customer->exp_term)->format('d/m/Y'),
'doc_date' => carbon(config('venice.config.sales.date'))->format('d/m/Y'),
'vat_system' => $this->customer->vat_system,
'bf_code' => $this->customer->bf_code,
'doc_type' => ($this->doc_type === 'slsCreditnote') ? 1 : 0,
'pdf' => base64_encode($pdf),
'pdfName' => $this->date->format('Ym') . '-' . $this->id . '.pdf',
'remark' => 'Clockwork ' . $this->date->format('Y F') . ' ' . $this->user->name,
'details' => $details,
];
}
For now I've added a temporary fix to mitigate the issue. I've created a clone of the $invoice. later I set the id of the original invoice to the cloned invoice id.
...
$invoice_copy = clone $invoice;
if ($invoice->to_export) {
DB::beginTransaction();
try {
$data = $service->export($invoice);
$invoice->book_date = Carbon::now();
$invoice->doc_num = $data['doc_num'];
$invoice->sys_num = $data['sys_num'];
$invoice->tsm_num = $data['tsm_num'];
$invoice->to_export = false;
$invoice->is_ticked = false;
$invoice->id = $invoice_copy->id;
$invoice->save();
DB::commit();
...
After a lot of debugging I have pinpointed where the id is set to true. I still don't know why.
In VeniceInvoiceResource $this->id before the PDF generation, the id is still the original invoice id. After the Printer, the id istrue.
If I look at the contructor for the resources, found in Illuminat\Http\Resources\JsonResource (Resource extends JsonResource) I see that $this->resource is set to the incomming value, in this case the $invoice.
/**
* Create a new resource instance.
*
* #param mixed $resource
* #return void
*/
public function __construct($resource)
{
$this->resource = $resource;
}
While in VeniceInvoiceResource $this->resource gets passed to the Printer instance. In the resource $this also has the values of the invoice.
/**
* Load items to print.
*
* #param $items
* #return $this
* #throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function print($items, $toPrint = null)
{
$items = is_array($items) || $items instanceof Collection ? $items : func_get_args();
foreach ($items as $item) {
if ($item instanceof Printable) {
foreach ($item->printData($toPrint) as $key => $data) {
switch($key) {
case 'merge':
$this->mergeOutput($data);
break;
case 'mergeFile':
$this->mergeFile($data);
break;
default:
$this->toPrint[] = $this->view->make($key, $data)->render();
}
}
} elseif ($item instanceof Renderable) {
$this->toPrint[] = $item->render();
} elseif (is_string($item)) {
$this->toPrint[] = $item;
} else {
throw new \InvalidArgumentException('Invalid argument');
}
}
return $this;
}
In the print method, $this->toPrint[] = $this->view->make($key, $data)->render(); gets used in this case. The output method looks like this.
/**
* Get the output as string.
*
* #return string
* #throws \iio\libmergepdf\Exception
*/
public function output()
{
return $this->generate();
}
/**
* Generate and merge.
*
* #return string
* #throws \iio\libmergepdf\Exception
*/
protected function generate()
{
$data = !empty($this->toPrint) ? $this->toPrint : [''];
$result = $this->pdf->getOutputFromHtml($data);
if (!empty($this->toMerge)) {
$this->merger->addRaw($result);
foreach ($this->toMerge as $item) {
$this->merger->addRaw($item);
}
$result = $this->merger->merge();
}
$this->toPrint = null;
$this->toMerge = null;
return $result;
}
In the print service nothing gets manipulated, it simply prints collections and items to a PDF format.
The last edit, because I found the line that caused all this. But I don't fully understand why it sets the id to true.
In Printer::print there is a call to a method on the model, printData() this method has an if statement to solve a problem we had with two invoices that needed some special treatment. There was not much time so we decided a simple if statement was sufficient enough for this situation.
if ($this->id = 4128 || $this->id === 4217) {
$vat_amount = 0;
$vat_label = '';
}
if you look closely you see that the first condition is not a condition... There is the problem, and the fix was simple. Remove this if statement as we don't need it any more. The invoices 4128 & 4217 already got printed and are archived. They do not need to be processed anymore.
Looks like you found the issue in your printData() method.
For why id ends up as true, it's due to the differing operator precedences.
The comparison operators (===) have a higher precedence than the logical operator (||), so the comparisons are done before the logical comparison. So, if the comparison operator had been correct, this is what would have been run (parens added for clarity):
($this->id === 4128) || ($this->id === 4217)
However, because the first operator was actually an assignment instead of a comparison, this changed the order of operation. The comparion and logical operators have a higher precedence than the assignment operator, so they are executed first. This is what was actually run (parens added for clarity):
$this->id = (4128 || $this->id === 4217)
So, id got assigned to the result of the logical comparison. Since all non-zero numbers evaluate to true, the logical comparison evaluated to true, and therefore id got set to true.
Let's take https://symfony.com/doc/current/forms.html#building-the-form example form but only for Search in tasks list instead of save.
Goal is to allow searches on task, dueDate or both criterias (in my real case, I have 9 criterias)
Here are src/Repository/ResultRepository.php :
class ResultRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry)
{
parent::__construct($registry, Result::class);
}
public function findMultiKeys($task, $dueDate): array
{
$qb = $this->createQueryBuilder('d')
->andWhere('d.task = :task')
->setParameter('task', $task)
->andWhere('d.dueDate = :dueDate')
->setParameter('dueDate', $dueDate)
->getQuery();
return $qb->execute();
}
}
It require both criterias to return result(s)!
I ran a lot of searches and find:
How do I use a complex criteria inside a doctrine 2 entity's
repository?
Symfony2/Doctrine QueryBuilder using
andwhere()
Doctrine2 doc - setParameters method
So I code a $where_string variable to construct my variable where.
and a $parameters to construct my variable parameters array, and my findMultiKeys becomes:
public function findMultiKeys($get_form): array
{
$where_string = '';
$parameters = [];
$task = $get_form->getTask();
if ($task !== null) {
$where_string .= "d.task = :task";
$parameters += array('task' => $task);
}
$dueDate = $get_form->getDueDate();
if ($dueDate !== null) {
if ($where_string !== '')
$where_string .= " AND ";
$where_string .= "d.dueDate = :dueDate";
$parameters += array('dueDate' => $dueDate);
}
$qb = $this->createQueryBuilder('d')
->where($where_string)
->setParameters($parameters)
->getQuery();
return $qb->execute();
}
It works, perhaps not the best way?
In my searches, I found, of course, to use ElasticSearch, perhaps to much to my simple need, or I found PetkoparaMultiSearchBundle
Bests solutions welcomes
I have a proposition :-)
You can build an task4Seach entity with your 9 criteria (without
persistance)
create a form type with this "data_class"
use it to get your datas in your repository
$qb = $this->createQueryBuilder('d');
...
if ($task4Seach ->getDueDate()) {
$qb->andWhere($qb->expr()->eq('d.dueDate', ':dueDate'));
$qb->setParameter('dueDate', $task4Seach->getDueDate());
}
I am checking the type of optional parameters in PHP like this:
/**
* Get players in the team using limit and
* offset.
*
*
* #param TeamInterface $participant
* #param int $limit
* #param int $offset
* #throws \InvalidArgumentException
* #return Players of a team
*/
public function getPlayers(TeamInterface $team, $limit = null, $offset = null)
{
if (func_num_args() === 2 && !is_int($limit) ){
throw new \InvalidArgumentException(sprintf('"Limit" should be of int type, "%s" given respectively.', gettype($limit)));
}
if (func_num_args() === 3 && (!is_int($limit) || !is_int($offset))){
throw new \InvalidArgumentException(sprintf('"Limit" and "Offset" should be of int type, "%s" and "%s" given respectively.', gettype($limit), gettype($offset)));
}
//.....
}
This works but there are 2 main issues with this:
1/ If I need to check the type of 4/5 optional parameters for the same int type, the code become unnecessarily long. Any ideas how to make this piece of code more maintainable? (Maybe use only one if statement to check the same type of both $limit and $offset)
2/ getPlayers($team, 2, null) throws an exception. Is this ok knowing that the function can actually handle a null value here?
You could do a for loop with an array of args. Something like:
$args = func_get_args();
for ($i = 1; $i < 4; $i++) {
if ($args[$i] !== null and !is_int($args[$i])) {
throw ...
}
}
Of course, adjust the for conditions based on your number of arguments that need to be checked.
Or...
$args = func_get_args();
// skip first
array_shift($args);
foreach ($args as $arg) {
if ($arg !== null and !is_int($arg)) {
throw ...
}
}
For 1) I would check each variable individually and throw an exception for each:
if (!is_int($limit)){
//Throw
}
if (!is_int($offset))){
//Throw
}
This still requires an if statement for each variable but is a bit less verbose.
For 2) if null values are allowed you can change the check to be something like:
if ($offset && !is_int($offset))){
//Throw
}
Finally I wouldn't recommend checking func_num_args(). In your example code calling your function with too many arguments would bypass the validation.
PHP doesn't have type hints for scalars yet.
Redesign
When you start to take a lot of optional arguments in your function you develop code smells. Something is wrong, there is an object waiting to emerge.
Build all of your optional parameters as an Object and have a validate method on it.
I think you want a GameParameters object and have a validate method on it.
getPlayers($gameParameters) {
}
Move your validation of the parameters to that object where you can build it into each setter or have a comprehensive validate() function.
Combinatorial problem
As far as the explosion of checks goes I would build an array of errors and throw that if there are errors. This can be done with or without redesign.
if ($limit != null && !is_int($limit){
#add to the errors array
}
if ($offset != null && !is_int($offset){
#add to the errors array
}
if (errors) {
throw new \InvalidArgumentException(sprintf('"Limit" and "Offset" should be of int type, "%s" and "%s" given respectively.', gettype($limit), gettype($offset)));
}
Personally I prefer to have only one argument per function (unless the function is very simple), For example the function can take $request, and returns a tree of data $response. It makes it a bit easier to loop over and extend later:
function dostuff( $request ) {
$team = #$request['team'];
$limit = #$request['limit'];
$offset = #$request['offset'];
// ...
return $response;
}
Then for validation, you can write a set of rules at the top of the function like
// define validation rules
$rules = array( 'required' => array('team'),
'depends' => array('offset' => 'limit'),
'types' => array('offset' => 'int', 'limit' => 'int' ),
);
And centralize all your error checking in one call:
// can throw exception
argcheck( array( 'request' => $request, 'rules' => $rules ) );
This might need optimization, but the general approach helps contain bloat as you increase the complexity of the functions.
Use switch to code specific functions.
switch(gettype($limit)) {
case "integer":
//do other processing
break;
}
You cannot leave your code vulnerable like that. As for a safe solution to overcome the vulnerabilities. Create a safe list like this.
public function getPlayers(TeamInterface $team, $limit = null, $offset = null) {
$safelist = array("var1" => "TeamInterface", "var2" => "integer", "var3" => "integer");
$args = function_get_args();
$status = true;
foreach($args as $key => $var) {
if(gettype($var)!=$safelist["var".$key]) {
$status = false;
break;
}
}
if(!$status) break;
//...........
}
So I'd like to use the call_user_func to pass data to an optional parameter of a function.
Here's the example of a code, the optional parameter $data represents a functional called data that was declared in another file. I just want it to be called by using call_user_func that will set the parameter with the function's name and call it within the createtable function, but doesn't seem to work.
I got the example from the PHP Manual, but createTable contains many parameters. How can I make call_user_func only assign the string data to the optional parameter $data set to NULL as default?
function createTable($database, $table, $patch,$data = NULL)
{
echo "INFO: Adding '$table'in database '$database'.\n";
$sql = "SHOW TABLES FROM $database WHERE Tables_in_$database='$table';";
$result = mysql_query($sql);
$result_count = mysql_num_rows($result);
if ( $result_count != 1 ) {
echo "ERROR: Can not find table '$table' in database '$database'.\n";
$result = mysql_query($patch);
if ( false === $result ) {
echo "ERROR: Adding Table '$table' in database '$database' ... Failed\n";
return false;
}
else {
echo "INFO: Adding Table '$table'in database '$database' ... Success\n";
// using the optional parameter here
$data();
return true;
}
} else {
if ( $result_count == 1 ) {
echo "ERROR: Table '$table'already in database '$database'.\n";
return false;
}
}
}
// Now I'm passing value to the optional parameter $ data that is NULL as default.
call_user_func('createTable', "data");
Even with call_user_func you have to pass all the parameters.
Anyway, call_user_func is intended for use when the name of the function isn't necessarily known up front. For instance, you might have several functions and a variable, and the variable contains the name of the function to call.
Personally I think it's on par with eval and variable variables: A horrible idea. After all, if you have $foo = "function_name"; then you can call $foo() and it will call function_name.
Anyway, back to the point, just call it as a normal function and give it the parameters it needs. Pass null if you have to.
you must pass value like this
call_user_func('createTable', $database, $table, $patch,$data);
or this for call from class
call_user_func(array(&$objectName->{$anotherObject},$functionName), $arg1, $arg2, $arg2);
or you can use this can get arg as array
call_user_func_array("createTable", array("one", "two"));
or this for call from class can get arg as array
call_user_func_array(array($foo, "bar"), array("three", "four"));
or This can help you too it not need to pass all args
function __named($method, array $args = array())
{
$reflection = new ReflectionFunction( $method);
$pass = array();
foreach($reflection->getParameters() as $param)
{
/* #var $param ReflectionParameter */
if(isset($args[$param->getName()]))
{
$pass[] = $args[$param->getName()];
}
else
{
try{
$pass[] = $param->getDefaultValue();
}catch(Exception $e){
$pass[] = NULL;
}
}
}
return $reflection->invokeArgs( $pass);
}
I hope It Work
sample:
__named('createTable', array('data' => 'value'));
and it is for use in class
public function __named($method, array $args = array())
{
$reflection = new ReflectionMethod($this, $method);
$pass = array();
foreach($reflection->getParameters() as $param)
{
/* #var $param ReflectionParameter */
if(isset($args[$param->getName()]))
{
$pass[] = $args[$param->getName()];
}
else
{
try{
$pass[] = $param->getDefaultValue();
}catch(Exception $e){
$pass[] = NULL;
}
}
}
return $reflection->invokeArgs($this,$pass);
}
if you Don't set any value __named Put Null instead of Unset Value
It seems you just want to pass the last param, and not worry about the 1st three. I don't think call_user_func is the right tool here at all.
Why not just make a function that calls your function?
function call_createTable($data){
$database = '...';
$table = '...';
$patch = '...';
return createTable($database, $table, $patch, $data);
}
Then just simply call it like this: call_createTable("data");.