I have the following Phql query:
$persons = Person::query()
->columns([
'id' => 'id',
'name' => 'first_name || last_name'
])
->where("first_name LIKE :searchQuery:")
->orWhere("last_name LIKE :searchQuery:")
->bind(['searchQuery' => $searchQuery . '%'])
->execute();
The database used is an sqlite database.
The query works fine until I include the concatenation operator of sqlite ||
The exception thrown is:
Scanner: Unknown opcode 402
I have tried CONCAT(first_name, last_name), but it throws the exception:
SQLSTATE[HY000]: General error: 1 no such function: CONCAT
Also much better is to use dialect extension:
namespace App/Dialect;
class Sqlite extends \Phalcon\Db\Dialect\Sqlite
{
/**
* Sqlite constructor.
*/
public function __construct()
{
$this->registerCustomFunctions();
}
/**
* Register Custom dialect functions
*/
public function registerCustomFunctions()
{
$customFunctions = [
'CONCAT_WS' => 'ConcatWs',
];
foreach ($customFunctions as $key => $value) {
$className = 'App\\Dialect\\Extensions\\'.$value;
$object = new $className;
$this->registerCustomFunction($key, $object->getFunction());
}
}
}
namespace App/Dialect/Extensions;
class ConcatWs
{
public function getFunction()
{
return function (Dialect $dialect, $expression) {
$sql = '';
$count = count($expression['arguments']);
if (true !== $count >= 2) {
throw new Exception('CONCAT_WS requires 2 or more parameters');
}
if (2 === $count) {
return $this->getSqlExpression($expression['arguments'][1]);
}
$separator = array_shift($expression['arguments']);
--$count;
foreach ($expression['arguments'] as $argument) {
$sql .= $this->getSqlExpression($argument);
if (0 !== --$count) {
$sql .= ' || '.$this->getSqlExpression($separator).' || ';
}
}
return $sql;
};
}
}
And then in your db service:
use App/Dialect/Sqlite as SqliteDialect;
$di->set('db', function() {
return new Sqlite([
// other options, like password etc
'dialectClass' => SqliteDialect::class
]);
});
Sadly this is not supported for sqllite, but you can extend the class and add support for MySQL concat function. Here is a working solution: https://forum.phalconphp.com/discussion/15233/concatenate-columns-using-sqlite
MySQL solution:
->columns([
'id',
'CONCAT_WS("#", id, created_at) AS concatenatedValue',
])
Result
Array
(
[0] => Array
(
[id] => 1
[concatenatedValue] => 1#2017-04-04 12:08:52
Related
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.
I want to short this function. I have also published a little part of it, but it is every time the same principle:
if(in_array($infinitiveVerb,
IrregularExceptionGroup::$name_in_lowercase)) {
$exceptionmodel = ExceptionModel::NAME_IN_UPPERCASE;
}
php function
function finding_exception_model(InfinitiveVerb $infinitiveVerb)
{
$exceptionmodel = ExceptionModel::NO_EXCEPTIONS;
if (in_array($infinitiveVerb, IrregularExceptionGroup::$aller)) {
$exceptionmodel = ExceptionModel::ALLER;
}
if (in_array($infinitiveVerb, IrregularExceptionGroup::$avoir_irr)) {
$exceptionmodel = ExceptionModel::AVOIR_IRR;
}
if (in_array($infinitiveVerb, IrregularExceptionGroup::$etre_irr)) {
$exceptionmodel = ExceptionModel::ETRE_IRR;
}
return new ExceptionModel($exceptionmodel);
}
ExceptionModel.php
class ExceptionModel extends Enum
{
const NO_EXCEPTIONS = 'no exceptions';
const ALLER = 'aller';
const AVOIR_IRR = 'avoir_irr';
const ETRE_IRR = 'etre_irr';
}
How is this possible?
The only thing I can see and would change here, is to just put every irregularExceptionGroup into an array, like this:
function finding_exception_model(InfinitiveVerb $infinitiveVerb)
{
$exceptionmodel = ExceptionModel::NO_EXCEPTIONS;
$irregularExceptionGroupArray = [
ExceptionModel::ALLER => IrregularExceptionGroup::$aller,
ExceptionModel::AVOIR_IRR => IrregularExceptionGroup::$avoir_irr,
ExceptionModel::ETRE_IRR => IrregularExceptionGroup::$etre_irr,
];
foreach($irregularExceptionGroupArray as $exceptionModel => $irregularExceptionGroup){
if(in_array($infinitiveVerb, $irregularExceptionGroup)){
$exceptionmodel = $exceptionModel;
//break; //If you don't want to overwrite the variable, just uncomment this
}
}
return new ExceptionModel($exceptionmodel);
}
You can combine all exceptions into one lookup, instead of this in_array checks all the time. (Note: IF off course the InfinitVerb has a __toString of some kind)
For example IrregularExceptionGroup::$aller contains: ['aller', 'allez', 'je suis', 'paris'] and IrregularExceptionGroup::$avoir_irr contains ['some', 'more', 'stuff']
Change it to:
IrregularExceptionGroup::$allExceptions = [
'aller' => ExceptionModel::ALLER,
'allez' => ExceptionModel::ALLER,
'je suis' => ExceptionModel::ALLER,
'paris' => ExceptionModel::ALLER,
'some' => ExceptionModel::AVOIR_IRR,
'more' => ExceptionModel::AVOIR_IRR,
'stuff' => ExceptionModel::AVOIR_IRR
];
function finding_exception_model(InfinitiveVerb $infinitiveVerb)
{
$ex = ExceptionModel::NO_EXCEPTIONS;
if (array_key_exists($infinitiveVerb, IrregularExceptionGroup::$allExceptions)) {
$ex = IrregularExceptionGroup::$allExceptions[$infinitiveVerb];
}
return new ExceptionModel($ex);
}
And you could even make it "shorter" by using the ternary operator:
function finding_exception_model(InfinitiveVerb $infinitiveVerb)
{
return new ExceptionModel(isset(IrregularExceptionGroup::$allExceptions[$infinitiveVerb]) ? IrregularExceptionGroup::$allExceptions[$infinitiveVerb] : ExceptionModel::NO_EXCEPTIONS);
}
or PHP 7:
function finding_exception_model(InfinitiveVerb $infinitiveVerb)
{
return new ExceptionModel( IrregularExceptionGroup::$allExceptions[$infinitiveVerb] ?? ExceptionModel::NO_EXCEPTIONS);
}
I have no idea why my query is suddenly not working (although maybe it was never working to begin with). Is there anything wrong here?
My controller;
$dataset = $postcode_lookup->dataset; // will return "201502_postcode"
$postcode_extract = new PostcodeExtract;
$postcode_extract = $postcode_extract->setTableByDate($dataset);
foreach ($input as $column => $values) {
$postcode_extract->orWhere(function ($query) use ($column, $values) {
$query->whereIn($column, $values);
});
}
/*
* Temporarily print out the raw SQL...
*/
$sql = str_replace(['%', '?'], ['%%', "'%s'"], $postcode_extract->toSql());
$fullSql = vsprintf($sql, $postcode_extract->getBindings());
print_r($fullSql);
exit;
My model;
<?php namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PostcodeExtract extends Model {
protected $connection = 'postcodes';
public function setTableByDate($selected_tablename)
{
$this->table = $selected_tablename;
// Return $this for method chaining
return $this;
}
public function getTable()
{
if (isset($this->table))
$this->setTableByDate($this->table);
return $this->table;
}
All that the print out of the raw sql is returning is select * from 201502_postcode and is just ignoring the rest of the query.
This is my $input array;
Array
(
[country] => Array
(
[0] => L93000001
[1] => M83000003
)
[county] => Array
(
[0] => 95
)
)
orWhere and whereIn is just being ignored it seems..
--- UPDATE ---
I didn't know about orWhereIn. But having tried this;
foreach ($input as $column => $values) {
$postcode_extract->orWhereIn($column, $values);
print "<br>";
print $column;
print "<br>";
print_r($values);
print "<br>";
}
I still get the same result. It's like this loop is being totally ignored - even though I can get these print's/print_r's to work. The raw SQL is still just select * from 201502_postcode.
Try this
$postcode_extract->orWhereIn($column, $values)
Also, if that doesnt work, try making the first one condition a whereIn and the rest a orWhereIn
Oh this is so frustratingly obvious I think I might cry...
foreach ($input as $column => $values) {
$postcode_extract = $postcode_extract->orWhereIn($column, $values);
}
I have these two methods in my Contact.php model:
public function getSubscribers($listId)
{
return $this->withTrashed()
->where(DB::raw("concat('',email * 1)"), '!=', DB::raw('email'))
->where('opt_out', '0')
->select('email')
->chunk(1000, function($results) use ($listId) { $this->subscribeEmails($listId, $results); });
}
public function subscribeEmails($listId, $subscribers)
{
$emails = array();
foreach ($subscribers as $key => $subscriber)
{
$memberActivity = $subscriber->memberActivity($listId);
if ( ! $memberActivity['data'])
{
$emails[] = array('email' => $subscriber->email);
}
else
{
foreach ($memberActivity['data'] as $data)
{
foreach ($data['activity'] as $activity)
{
if ($activity['action'] !== 'unsub')
{
$emails[] = array('email' => $subscriber->email);
}
}
}
}
}
MailchimpWrapper::lists()->batchSubscribe($listId, $emails, false, true);
}
And the getSubscribers() method is called in my AdminContactsController.php controller via a method called updateMailchimp():
public function updateMailchimp()
{
$this->contact->getSubscribers($this->listId);
$message = (object) array(
'title' => 'Excellent!',
'content' => 'The Mailchimp newsletter list has been updated with the latest contacts from within the system.',
'alert_type' => 'success'
);
return Redirect::back()->with('message', $message);
}
Locally, this works great, no problems at all but on the staging server, I get the following error referencing the line cotaining ->chunk(1000, function($results) use ($listId) { $this->subscribeEmails($listId, $results); });:
Using $this when not in object context
Is this a PHP version issue or am I missing something here?
The reason why your code works on localhost but not on the remote server is probably the difference in PHP versions. Before PHP 5.4.0 it is not possible to use $this from anonymous function. You must pass the reference to $this within the use keyword:
public function getSubscribers($listId)
{
$that = $this; // <---- create reference to $this
return $this->withTrashed()
->where(DB::raw("concat('',email * 1)"), '!=', DB::raw('email'))
->where('opt_out', '0')
->select('email')
->chunk(1000, function($results) use (&$that, $listId) { $this->subscribeEmails($listId, $results); });
}
I'm a little bit baffled by this, so I'm hoping someone can shed some light on it for me.
I have a function which is meant to return a column Status from one of my tables, visit.
function someFunction($visitId = null) {
$visit = VisitQuery::create()
->select(array('Status'))
->findPk($visitId);
}
If I var_dump($visit), when calling the function for the first time, it outputs:
string '1' (length=1)
Subsequent, identical calls to the function however seem to return an entire object:
object(Visit)[30]
protected 'startCopy' => boolean false
protected 'id' => int 362
protected 'job_id' => int 351
protected 'company_id' => int 2
protected 'type_id' => int 1
protected 'visit_date' => string '2013-08-23 00:00:00' (length=19)
protected 'status' => string '1' (length=1)
...
I'm calling the function for the first time with an (int) $visitId passed via a posted form:
var_dump($visitId); // int 362
Subsequent calls are made with an (int) $visitId which is returned from another function, saveVisit() (which uses Propel to save the record - I believe this may have something to do with it).
$visitId = saveVisit($visitId);
var_dump($visitId); // int 362
I tried to do some debugging, and for some reason the query issued to MySQL is different between the first function call and subsequent ones:
var_dump($con->getLastExecutedQuery());
SELECT visit.STATUS AS "Status" FROM `visit` WHERE visit.ID=362 // 1st call
SELECT `ID`, `JOB_ID`, `COMPANY_ID`, `TYPE_ID`, `VISIT_DATE`, `STATUS`, `REMIND`, `PRINTED` FROM `visit` WHERE `ID` = 362 // 2nd call
SELECT `ID`, `JOB_ID`, `COMPANY_ID`, `TYPE_ID`, `VISIT_DATE`, `STATUS`, `REMIND`, `PRINTED` FROM `visit` WHERE `ID` = 362 // 3rd call
Can anyone tell me why or how this is happening?
I'm using Propel 1.6.
Propel's create() method:
public static function create($modelAlias = null, $criteria = null)
{
if ($criteria instanceof VisitQuery) {
return $criteria;
}
$query = new VisitQuery();
if (null !== $modelAlias) {
$query->setModelAlias($modelAlias);
}
if ($criteria instanceof Criteria) {
$query->mergeWith($criteria);
}
return $query;
}
Propel's findPk() method:
public function findPk($key, $con = null)
{
if ($con === null) {
$con = Propel::getConnection($this->getDbName(), Propel::CONNECTION_READ);
}
// As the query uses a PK condition, no limit(1) is necessary.
$this->basePreSelect($con);
$criteria = $this->isKeepQuery() ? clone $this : $this;
$pkCols = $this->getTableMap()->getPrimaryKeyColumns();
if (count($pkCols) == 1) {
// simple primary key
$pkCol = $pkCols[0];
$criteria->add($pkCol->getFullyQualifiedName(), $key);
} else {
// composite primary key
foreach ($pkCols as $pkCol) {
$keyPart = array_shift($key);
$criteria->add($pkCol->getFullyQualifiedName(), $keyPart);
}
}
$stmt = $criteria->doSelect($con);
return $criteria->getFormatter()->init($criteria)->formatOne($stmt);
}
My function to retrieve the visit status:
function getVisitStatus($visitId = null) {
if (empty($visitId)) {
return false;
}
try {
$visit = VisitQuery::create()
->select(array('Status'))
->findPk($visitId);
} catch (Exception $e) {
echo $e->getMessage(); exit;
}
if (is_null($visit)) {
return false;
}
return $visit;
}
The function which saves a visit record:
function saveVisit($data = null) {
if (empty($data)) {
return false;
}
try {
$visit = (!empty($data['visit_id'])) ? VisitQuery::create()->findPk($data['visit_id']) : new Visit();
if (!is_object($visit)) {
return false;
}
$visitDataMap = array(
'JobId' => 'job_id',
'TypeId' => 'type_id',
'VisitDate' => 'visit_date',
'Status' => 'status',
'CompanyId' => 'engineer_id',
'Remind' => 'remind'
);
$visitData = array();
foreach ($visitDataMap as $column => $value) {
if (!empty($data[$value])) {
$visitData[$column] = $data[$value];
}
}
$visit->fromArray($visitData);
$visit->save();
return $visit->getId();
} catch (PropelException $e) {
echo $e->getMessage(); exit;
}
}
It seems that on the first call will grab your data but then put a copy of the full object in to the instance pool. I am not sure if this is a bug or valid behaviour (your code would suggest the former, but I'd love to hear from someone who knows more about Propel such as a dev) but you can stop it happening with:
VisitPeer::clearInstancePool();
Bear in mind you're losing a bit of caching from other queries you might have done here with the visit relation.
You will be able to confirm this by adding an echo in the BaseVisitPeer.php file. You will see something like this:
public static function getInstanceFromPool($key)
{
if (Propel::isInstancePoolingEnabled()) {
if (isset(VisitPeer::$instances[$key])) {
return VisitPeer::$instances[$key];
}
}
return null; // just to be explicit
}
If you add an echo somewhere inside the if (isset(VisitPeer::$instances[$key])) { statement, you should see it appear only after the second and subsequent calls. If you were to comment this whole if statement out then you would get the same result back each time - the one you correctly get back from your original call.
Hope that helps!