Phalcon\Mvc\Model::find() together with Phalcon\Mvc\Model\Query\Builder - php

I am trying to use Phalcon\Mvc\Model\Query\Builder object together with Phalcon\Mvc\Model::find() method to customize data load behaviour.
All model rows in the table have "record_status" field which is used to mark records active/inactive.
I intend to extend Model::find() and Model::findFirst() methods to always add "record_status=1" constraint to all my Model queries. But for now, I'm just trying to pass Query\Builder object on to ::find() method externally:
class User extends Phalcon\Mvc\Model
{
}
$user = new User();
/**
* #var Phalcon\Mvc\Model\Query\Builder
*/
$query = $user->getModelsManager()->createBuilder();
$query->where('email = :email:', [
'email' => 'root#yahoo.com'
])->andWhere('record_status = :status:', [
'status' => 1
])->from('users');
$found = $user->find($query);
foreach ($found as $foundUser) {
...
}
The problem is that ->find($query) returns ALL rows from the database, ignoring WHERE clauses set to $query.
When inspecting properties of $query & $user I see the following relevant protected properties:
$query::_conditions = '(email = :email:) AND (record_status = :status:)';
$query::_bindParams = array(
'email' => 'root#yahoo.com',
'status' => 1
);
$user::_count = 4; // This is wrong, this corresponds to TOTAL number of rows
$user::_result->_bindParams = NULL; // Bound parameters have disappeared
$user::_result->_sqlStatement = 'SELECT `users`.`id`, `users`.`email`, `users`.`record_status` FROM `users`'; // As you can see, there is no WHERE clause
I'm on Phalcon 1.3.0, PHP 5.5.1.
I expect find() and findFirst() methods to accept Query\Builder() object and fetch the correct records. Is this a bug or am I approaching it incorrectly?
Thanks,
Temuri

Yep you have things wrong.
The Model::find() and Model::findFirst() functions accept arrays, int or string and are wrappers for query builder.
You have at least two options:
a) use query builder directly:
// ... here is your query builder script as in question
$results = $query->getQuery()->execute();
$firstFound = $results->getFirst();
b) pass array to Model::findFirst():
$firstFound = User::findFirst(
array(
'(email = :email:) AND (record_status = :status:)',
'bind' => array('email' => 'root#yahoo.com', 'status' => 1)
)
);

Related

Zend Framework 2/3 model with relations to itself and another model

First I've read the following two stackoverflow questions, but they didn't really give me an answer:
How to extend the ZF2 skeleton application - entities with foreign keys
Zend Framework 2 model foreign keys
In my application I have an employee database table, which has numerous properties but the most interesting for me currently is the manager_id and the bank_id which are foreign keys.
manager_id is a foreign key to another employee (as you can imagine an employee can have one manager)
bank_id is a foreign key to another model/db-table called bank - because an employee can have a bank account ;-)
Now in my EmployeeTable.php file I have those magic methods where I get the database results.
To get one employee I do this:
/**
* #param int $id
*
* #return Employee
*/
public function getEmployeeById($id)
{
$rowset = $this->tableGateway->select(['id' => (int) $id]);
/** #var Employee $row */
$row = $rowset->current();
if (!$row) {
throw new RuntimeException(sprintf(
'Could not find row with identifier %d',
(int) $id
));
}
return $row;
}
But without any sql joins I only have the manager_id and bank_id in my returned employee object.
Question: What is the best practise to get those needed information?
So far I have two thoughts:
First
Should I - if the $row isn't empty - call for e.g the bankTable object (via dependency injection) which has an getBankById method.
Then I'll extend my Employee.php model with an $bank property with a getter/setter and before the return statement in my getEmployeeId method I would do something like this:
$row->setBank($this->bankTable->getBankById($row->bank_id));
But I'm afraid of a recursive loop doing this for the manager_id because I would call the same method I'm currently in.
Second
Or should I extend my getEmployeeById method with a left join to get the data from the bank table like this:
$select = $this->tableGateway->getSql()->select()
->join(['b' => 'bank'], 'bank_id = m.id',
[
'bank.id' => 'id',
'bank.description' => 'description',
'bank.bic' => 'bic',
],
Select::JOIN_LEFT)
->join(['m' => 'employee'], 'manager_id = m.id',
[
'manager.id' => 'id',
'manager.forename' => 'forename',
'manager.surname' => 'surname',
// and all the other properties
],
Select::JOIN_LEFT);
$result = $this->tableGateway->selectWith($select);
$row= $result->current();
$resultSet = $this->hydrator->hydrate($this->hydrator->extract($row), $row);
Unfortunately I have to give my joined columns alias names else I would overwrite the id from the employee with the bank id etc.
After this kind of sql statement you can see, that I would extract the result to get the properties as values and then hydrate them.
Hydration would look like this:
/**
* #param array $data
* #param Employee $object
*
* #return Employee
*/
public function hydrate(array $data, $object)
{
if (!$object instanceof Employee) {
throw new \BadMethodCallException(sprintf(
'%s expects the provided $object to be a PHP Employee object)',
__METHOD__
));
}
$employee = new Employee();
$employee->exchangeArray($data);
$bank = new Bank();
$bank->exchangeArray($data, 'bank.');
$employee->setBank($bank);
$manager = new Employee();
$manager->exchangeArray($data, 'manager.');
$employee->setManager($manager);
return $employee;
}
As a result of this I have a clean employee model (without those extra alias columns) and additionally 2 new properties which are objects of another employee(manager) and the bank.
But this looks quite overloaded...
Thanks for reading so far - If you have any hints or advices they are warmly welcomed!
EDIT
I've edited my EmployeeTableFactory to do the following (about hydrating):
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$dbAdapter = $container->get(AdapterInterface::class);
$resultSetPrototype = new HydratingResultSet();
$resultSetPrototype->setHydrator(new EmployeeHydrator());
$resultSetPrototype->setObjectPrototype(new Employee());
$tableGateway = new TableGateway('employee', $dbAdapter, null, $resultSetPrototype);
return new EmployeeTable($tableGateway);
}
I changed my EmployeeHydrator to implement the HydratorInterface because I was already using the extract stuff and now it matches the necessary interface for the resultSetPrototype->setHydrator() method.
Now things are getting pretty easy in the getEmployeeById method because with the following code I already have my finished employee object and all related foreign key objects (due to my EmployeeHydrator)
$result = $this->tableGateway->selectWith($select);
$row = $result->current(); // the given employee object result already hydrated!
return $row;
I kind of like this implementation

Can I merge a ArrayDataProvider and ActiveDataProvider?

Using Yii2 I'll need to provide a DataProvider with specific data.
The problem is that I need to create a single DataProvider with an array data and data from a table in a database.
Therefore, I think that I should mix an ArrayDataProvider and an ActiveDataProvider. I don't know if Yii2 offers tools for this situations or should I create my own DataProvider from BaseDataProvider.
I cant post some code because is a knowledge question.
According to your description, you could try to get the data from the DB as an Array, then merge it with the other array you have, and then put it all in an ArrayDataProvider
Try this (not tested):
$dbData = Model::find()->all(); //load your data from DB
$arrayData = [...]; //your other data as an array
// do the merge, with ArrayHelper::merge() or somehow manually
$data = ArrayHelper::merge($dbData, $arrayData);
$provider = new ArrayDataProvider([
'allModels' => $data
]);
Yes, Asped's approach will work. Here's a working example, in a Model function - in this case I'm merging a SqlDataProvider and an ActiveDataProvider, to merge data from two tables, but the same principle would apply to merging any other combination of DataProviders (yes, I did test the other combinations):
/**
* #return ArrayDataProvider;
*
* Combine current Activity with Archive:
*/
public function getActivityCombined() {
# Current:
$dataProviderActivityCurrent = new SqlDataProvider([
'sql' => 'SELECT listing_id, counter_reset, impress_town, impress_county,
impress_listing, clicks_web, clicks_facebook, clicks_twitter
FROM listing WHERE listing_id = ' . $this->listing_id,
]);
# Archive:
$query = ActivityArchive::find();
$dataProviderActivityArchive = new ActiveDataProvider([
'query' => $query,
]);
$dataProviderActivityArchive->query->andWhere(['listing_id' => $this->listing_id]);
# Merge:
$dataMerge = array_merge($dataProviderActivityCurrent->getModels(), $dataProviderActivityArchive->getModels());
$dataProviderActivity = new ArrayDataProvider([
'allModels' => $dataMerge
]);
# Return combined:
return $dataProviderActivity;
}
Of course you'll need to include use statements for the three DataProvider types.
The model function is then called by a GridView widget in a view:
echo GridView::widget([
'dataProvider' => $model->getActivityCombined(),
'columns' => [
'counter_reset',
'impress_town',
'impress_county',
'impress_listing',
'clicks_web',
'clicks_facebook',
'clicks_twitter',
],
]);

Yii2 ActiveQuery use OR in Link array

I want to use OR operator in $link array in hasMany function in class extended by ActiceRecord.
For example, I want to get transactions which related whith user account. In sql it would be something like SELECT * FROM transactions WHERE fromAccountId = :id OR toAccountId = :id But how I can wrote this using Yii2
public function getTransactions() {
return $this->hasMany(Transaction::className(), [
'fromAccountId' => 'id',
'toAccountId' => 'id'
]);
}
Link ActiveQuery works with the keys of the array as name column, values - as value of column.
The array keys must be columns of the table for this relation, and the array values must be the corresponding columns from the primary table
Because the code doesn't work (where (fromAccountId, toAccountId) IN ('id','id')):
[
'fromAccountId' => 'id',
'toAccountId' => 'id'
]
You can rewrite hasMany behavior in getTransactions()
public function getTransactions() {
$query = Transaction::find();
$query->multiple = true;
return $query->where(['OR',
['fromAccountId' => $this->id],
['toAccountId' => $this->id]
]);
}
It supports native behavior, as expected:
$model->getTransactions() // return as \yii\db\ActiveQuery
$model->transactions // return as array of Transactions models
But doesn't work for $model->find()->with('transactions'), because with require setting $query->link. Instead with need to use join....
You can use the find(), it's not as nice, but do the work:
return $this->find()->join('LEFT JOIN', 'transaction', 'fromAccountId = id OR toAccountId = id')->all();
Maybe you have to use tablename.id!
I have not tried this, but you could try something like
public function getTransactions() {
return $this->hasMany(Transaction::className(), ['1' => '1'])->where('fromAccountId = id OR toAccountId = id');
}
The idea is to create a join without a condition (or with a dummy condition) then use where to get the actual results you want. This might mean a massive performance problem.

How do I get database column names in Laravel?

How can I get column names of a table in an array or object in Laravel 4, using Schema, DB, or Eloquent?
It seems that I can't find a ready to use function, maybe you have some custom implementations.
New Answer
At the time I gave this answer Laravel hadn't a way to do this directly, but now you can just:
$columns = Schema::getColumnListing('users');
Old Answer
Using attributes won't work because if you do
$model = new ModelName;
You have no attributes set to that model and you'll get nothing.
Then there is still no real option for that, so I had to go down to the database level and this is my BaseModel:
<?php
class BaseModel extends \Eloquent {
public function getAllColumnsNames()
{
switch (DB::connection()->getConfig('driver')) {
case 'pgsql':
$query = "SELECT column_name FROM information_schema.columns WHERE table_name = '".$this->table."'";
$column_name = 'column_name';
$reverse = true;
break;
case 'mysql':
$query = 'SHOW COLUMNS FROM '.$this->table;
$column_name = 'Field';
$reverse = false;
break;
case 'sqlsrv':
$parts = explode('.', $this->table);
$num = (count($parts) - 1);
$table = $parts[$num];
$query = "SELECT column_name FROM ".DB::connection()->getConfig('database').".INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = N'".$table."'";
$column_name = 'column_name';
$reverse = false;
break;
default:
$error = 'Database driver not supported: '.DB::connection()->getConfig('driver');
throw new Exception($error);
break;
}
$columns = array();
foreach(DB::select($query) as $column)
{
$columns[] = $column->$column_name;
}
if($reverse)
{
$columns = array_reverse($columns);
}
return $columns;
}
}
Use it doing:
$model = User::find(1);
dd( $model->getAllColumnsNames() );
You may try Schema::getColumnListing('tablename'):
$columns = Schema::getColumnListing('users'); // users table
dd($columns); // dump the result and die
Result would be something like this depending on your table:
array (size=12)
0 => string 'id' (length=2)
1 => string 'role_id' (length=7)
2 => string 'first_name' (length=10)
3 => string 'last_name' (length=9)
4 => string 'email' (length=5)
5 => string 'username' (length=8)
6 => string 'password' (length=8)
7 => string 'remember_token' (length=14)
8 => string 'bio' (length=3)
9 => string 'created_at' (length=10)
10 => string 'updated_at' (length=10)
11 => string 'deleted_at' (length=10)
You can dig down into DB's Doctrine instance.
$columns = DB::connection()
->getDoctrineSchemaManager()
->listTableColumns('table');
foreach($columns as $column) {
print $column->getName();
print $column->getType()->getName();
print $column->getDefault();
print $column->getLength();
}
edit: Doctrine is no longer (as of L4.1) installed by default (it's a 'suggested' rather than 'required' package), but can be added to your composer.json as doctrine/dbal to retain this functionality.
I think there's a couple different options, if you are using an Eloquent model, you can look at the getAccessibleAttributes() method, which in theory would give you all the columns of a model consider Eloquent seems them as properties.
For example, you'd be able to do something like this for your users table on a User Eloquent model.
$user = // Retrieve your User model.
$columns = User->getAccessibleAttributes();
Another Eloquent method to look at that's similar, but doesn't have the 'accessibility' requirement is the attributesToArray() method. The returned array of which should have your columns as a key. Then you can use the PHP function array_keys() to build an array of the keys, which would be your columns.
$user = // Retrieve your User model.
$columns = array_keys(User::attributesToArray());
I know it might not be the answer for everyone, but maybe you can grab one record, and get all keys of the data. Ex.
array_keys(User::first()->toArray());
If you have a Model instance you can retrieve like following:
$table_name = $model->getTable();
$connection = $model->getConnection();
$schemaBulder = $connection->getSchemaBuilder();
$columns_array = $schemaBulder->getColumnListing($table_name);
works for Laravel 5
You also can try this:
abstract class BaseModel extends Eloquent {
public function getColumnsNames()
{
$connection = DB::connection();
$connection->getSchemaBuilder();
$table = $connection->getTablePrefix() . $this->table;
$grammar = $connection->getSchemaGrammar();
$results = $connection->select($grammar->compileColumnExists(), array($connection->getDatabaseName(), $table));
return $connection->getPostProcessor()->processColumnListing($results);
}
}
I use SQL Server and the Schema way worked for me:
$columns = array_keys(Schema::getConnection()
->getDoctrineSchemaManager()
->listTableColumns($yourModel->getTable()) );

Creating and Update Laravel Eloquent

What's the shorthand for inserting a new record or updating if it exists?
<?php
$shopOwner = ShopMeta::where('shopId', '=', $theID)
->where('metadataKey', '=', 2001)->first();
if ($shopOwner == null) {
// Insert new record into database
} else {
// Update the existing record
}
Here's a full example of what "lu cip" was talking about:
$user = User::firstOrNew(array('name' => Input::get('name')));
$user->foo = Input::get('foo');
$user->save();
Below is the updated link of the docs which is on the latest version of Laravel
Docs here: Updated link
2020 Update
As in Laravel >= 5.3, if someone is still curious how to do so in easy way it's possible by using: updateOrCreate().
For example for the asked question you can use something like:
$matchThese = ['shopId'=>$theID,'metadataKey'=>2001];
ShopMeta::updateOrCreate($matchThese,['shopOwner'=>'New One']);
Above code will check the table represented by ShopMeta, which will be most likely shop_metas unless not defined otherwise in the model itself.
And it will try to find entry with
column shopId = $theID
and
column metadateKey = 2001
and if it finds then it will update column shopOwner of found row to New One.
If it finds more than one matching rows then it will update the very first row that means which has lowest primary id.
If not found at all then it will insert a new row with:
shopId = $theID,metadateKey = 2001 and shopOwner = New One
Notice
Check your model for $fillable and make sure that you have every column name defined there which you want to insert or update and rest columns have either default value or its id column auto incremented one.
Otherwise it will throw error when executing above example:
Illuminate\Database\QueryException with message 'SQLSTATE[HY000]: General error: 1364 Field '...' doesn't have a default value (SQL: insert into `...` (`...`,.., `updated_at`, `created_at`) values (...,.., xxxx-xx-xx xx:xx:xx, xxxx-xx-xx xx:xx:xx))'
As there would be some field which will need value while inserting new row and it will not be possible, as either it's not defined in $fillable or it doesn't have a default value.
For more reference please see Laravel Documentation at:
https://laravel.com/docs/5.3/eloquent
One example from there is:
// If there's a flight from Oakland to San Diego, set the price to $99.
// If no matching model exists, create one.
$flight = App\Flight::updateOrCreate(
['departure' => 'Oakland', 'destination' => 'San Diego'],
['price' => 99]
);
which pretty much clears everything.
Query Builder Update
Someone has asked if it is possible using Query Builder in Laravel. Here is reference for Query Builder from Laravel docs.
Query Builder works exactly the same as Eloquent so anything which is true for Eloquent is true for Query Builder as well. So for this specific case, just use the same function with your query builder like so:
$matchThese = array('shopId'=>$theID,'metadataKey'=>2001);
DB::table('shop_metas')::updateOrCreate($matchThese,['shopOwner'=>'New One']);
Of course, don't forget to add DB facade:
use Illuminate\Support\Facades\DB;
OR
use DB;
Updated: Aug 27 2014 - [updateOrCreate Built into core...]
Just in case people are still coming across this... I found out a few weeks after writing this, that this is in fact part of Laravel's Eloquent's core...
Digging into Eloquent’s equivalent method(s). You can see here:
https://github.com/laravel/framework/blob/4.2/src/Illuminate/Database/Eloquent/Model.php#L553
on :570 and :553
/**
* Create or update a record matching the attributes, and fill it with values.
*
* #param array $attributes
* #param array $values
* #return static
*/
public static function updateOrCreate(array $attributes, array $values = array())
{
$instance = static::firstOrNew($attributes);
$instance->fill($values)->save();
return $instance;
}
Old Answer Below
I am wondering if there is any built in L4 functionality for doing this in some way such as:
$row = DB::table('table')->where('id', '=', $id)->first();
// Fancy field => data assignments here
$row->save();
I did create this method a few weeks back...
// Within a Model extends Eloquent
public static function createOrUpdate($formatted_array) {
$row = Model::find($formatted_array['id']);
if ($row === null) {
Model::create($formatted_array);
Session::flash('footer_message', "CREATED");
} else {
$row->update($formatted_array);
Session::flash('footer_message', "EXISITING");
}
$affected_row = Model::find($formatted_array['id']);
return $affected_row;
}
I would love to see an alternative to this if anyone has one to share.
firstOrNew will create record if not exist and updating a row if already exist.
You can also use updateOrCreate here is the full example
$flight = App\Flight::updateOrCreate(
['departure' => 'Oakland', 'destination' => 'San Diego'],
['price' => 99]
);
If there's a flight from Oakland to San Diego, set the price to $99. if not exist create new row
Reference Doc here: (https://laravel.com/docs/5.5/eloquent)
Save function:
$shopOwner->save()
already do what you want...
Laravel code:
// If the model already exists in the database we can just update our record
// that is already in this database using the current IDs in this "where"
// clause to only update this model. Otherwise, we'll just insert them.
if ($this->exists)
{
$saved = $this->performUpdate($query);
}
// If the model is brand new, we'll insert it into our database and set the
// ID attribute on the model to the value of the newly inserted row's ID
// which is typically an auto-increment value managed by the database.
else
{
$saved = $this->performInsert($query);
}
If you need the same functionality using the DB, in Laravel >= 5.5 you can use:
DB::table('table_name')->updateOrInsert($attributes, $values);
or the shorthand version when $attributes and $values are the same:
DB::table('table_name')->updateOrInsert($values);
$shopOwner = ShopMeta::firstOrNew(array('shopId' => $theID,'metadataKey' => 2001));
Then make your changes and save. Note the firstOrNew doesn't do the insert if its not found, if you do need that then its firstOrCreate.
Like the firstOrCreate method, updateOrCreate persists the model, so there's no need to call save()
// If there's a flight from Oakland to San Diego, set the price to $99.
// If no matching model exists, create one.
$flight = App\Flight::updateOrCreate(
['departure' => 'Oakland', 'destination' => 'San Diego'],
['price' => 99]
);
And for your issue
$shopOwner = ShopMeta::updateOrCreate(
['shopId' => $theID, 'metadataKey' => '2001'],
['other field' => 'val' ,'other field' => 'val', ....]
);
One more option if your id isn't autoincrement and you know which one to insert/update:
$object = MyModel::findOrNew($id);
//assign attributes to update...
$object->save();
Actually firstOrCreate would not update in case that the register already exists in the DB.
I improved a bit Erik's solution as I actually needed to update a table that has unique values not only for the column "id"
/**
* If the register exists in the table, it updates it.
* Otherwise it creates it
* #param array $data Data to Insert/Update
* #param array $keys Keys to check for in the table
* #return Object
*/
static function createOrUpdate($data, $keys) {
$record = self::where($keys)->first();
if (is_null($record)) {
return self::create($data);
} else {
return self::where($keys)->update($data);
}
}
Then you'd use it like this:
Model::createOrUpdate(
array(
'id_a' => 1,
'foo' => 'bar'
), array(
'id_a' => 1
)
);
like #JuanchoRamone posted above (thank #Juancho) it's very useful for me, but if your data is array you should modify a little like this:
public static function createOrUpdate($data, $keys) {
$record = self::where($keys)->first();
if (is_null($record)) {
return self::create($data);
} else {
return $record->update($data);
}
}
Isn't this the same as updateOrCreate()?
It is similar but not the same. The updateOrCreate() will only work
for one row at a time which doesn't allow bulk insert.
InsertOnDuplicateKey will work on many rows.
https://github.com/yadakhov/insert-on-duplicate-key
Try more parameters one which will surely find and if available update and not then it will create new
$save_data= Model::firstOrNew(['key1' => $key1value,'key'=>$key2value]);
//your values here
$save_data->save();
UpdateOrCreate method means either update or creates by checking where condition.
It is simple as in the code you can see, in the users table, it will check if an email has the value $user->email then it will update the data (which is in the 2nd param as an array) or it will create a data according to it.
$newUser = User::updateOrCreate(['email' => $user->email],[
'name' => $user->getName(),
'username' => $user->getName().''.$user->getId(),
'email' => $user->getEmail(),
'phone_no' => '',
'country_id' => 0,
'email_verified_at' => Carbon::now()->toDateTimeString(),
'is_email_verified' => 1,
'password'=>Hash::make('Secure123$'),
'avatar' => $user->getAvatar(),
'provider' => 'google',
'provider_id' => $user->getId(),
'access_token' => $user->token,
]);
check if a user exists or not. If not insert
$exist = DB::table('User')->where(['username'=>$username,'password'=>$password])->get();
if(count($exist) >0) {
echo "User already exist";;
}
else {
$data=array('username'=>$username,'password'=>$password);
DB::table('User')->insert($data);
}
Laravel 5.4

Categories