From what I understand, the best way to deal with dates in the Zend Framework is to select them as a Unix timestamp from the database.
Quick Creation of Dates from Database Date Values
// SELECT UNIX_TIMESTAMP(my_datetime_column) FROM my_table
$date = new Zend_Date($unixtimestamp, Zend_Date::TIMESTAMP);
I think it's a pain that there is actually no easy way in Oracle to either select dates as Unix timestamps or in ISO-8601 format - which are the two formats Zend_Date knows best.
But I did write a function to select dates as unix timestamps in PL/SQL, so I can actually do this now.
Using Zend_Db_Expr, I can now select my dates as Unix timestamps:
$select = $db->select()
->from(array('p' => 'products'),
array(
'product_id',
'product_date' => new Zend_Db_Expr('toUnixTimestamp(product_date)')
)
);
$results = $db->fetchAll($select);
You would use a similar query for any RDMS - most have a timestamp function.
I find this anoying because now I have to loop through $results to transform the timestamp to a Zend_Date object manually:
foreach($results as $result){
$productDate = new Zend_Date($result['product_date'], Zend_Date::TIMESTAMP);
echo $productDate->toString('dd/MMM/yyyy HH:mm:ss');
}
I want my Model to return $results where the timestamps are already transformed to Zend_Date. I don't want to have to write a loop in every data-access function to do this for me.
So to get to the point of my actual question:
Does anyone know of a way with Zend_Db, to set up some sort of post-processing on the result set, thus converting the timestamps to Zend_Date objects automatically?
I've encountered scenarios where I've wanted to do this. Here is the solution that I've used:
Created an extended row class for Zend_Db_Table_Row and overloaded the __get() and __set() super-methods
In the specific classes/tables that I want to use date objects, created the appropriate methods to do the heavy lifting
Here is a dumbed-down version of the extended row class that I use on my projects:
/**
* #category FireUp
* #package FireUp_Db
* #copyright Copyright (c) 2007-2009 Fire Up Media, Inc. (http://www.fireup.net)
* #license http://dev.fireup.net/license/mit MIT License
* #uses Zend_Db_Table_Row
*/
class FireUp_Db_Table_Row extends Zend_Db_Table_Row_Abstract
{
/**
* Retrieve row field value
*
* Checks for the existence of a special method to apply additional handling for the field data and calls the method if it exists
*
* #param string $columnName The user-specified column name.
* #return string The corresponding column value.
* #throws Zend_Db_Table_Row_Exception if the $columnName is not a column in the row.
*/
public function __get($key)
{
$inflector = new Zend_Filter_Word_UnderscoreToCamelCase();
$method = '_get' . $inflector->filter($key);
if (method_exists($this, $method))
{
return $this->{$method}();
}
return parent::__get($key);
}
/**
* Set row field value
*
* Checks for the existence of a special method to apply additional handling for the field data and calls the method if it exists
*
* #param string $columnName The column key.
* #param mixed $value The value for the property.
* #return void
* #throws Zend_Db_Table_Row_Exception
*/
public function __set($key, $value)
{
$inflector = new Zend_Filter_Word_UnderscoreToCamelCase();
$method = '_set' . $inflector->filter($key);
if (method_exists($this, $method))
{
return $this->{$method}($value);
}
return parent::__set($key, $value);
}
}
For our individual table classes, we override the functions as such:
class EntityRecord extends FireUp_Db_Table_Row
{
protected function _getDateCreated()
{
return new Zend_Date($this->_data['date_created'], Zend_Date::ISO_8601);
}
protected function _setDateCreated($value)
{
if ($value instanceof Zend_Date)
{
$value = $value->toString('YYYY-MM-dd HH:mm:ss');
}
$this->_data['date_created'] = $value;
$this->_modifiedFields['date_created'] = true;
}
}
Now, creating a new Zend_Date object everytime that the field would be accessed has some overhead, so in our classes, we take additional measures to cache the date objects, etc, but I didn't want that to get in the way of showing you the solution.
Use Zend_Table and have your table return you custom row objects that extend Zend_Db_Table_Row_Abstract. Then just have a method on that row like
function getDate() {
return new Zend_Date($this->datecol, Zend_Date::TIMESTAMP);
}
Related
Question:
I Noticed interesting behavior in Laravel 7.x where eager loaded relationships don't always have bindings. Is this expected behavior and why would that be the case?
Code:
Actual Queries Laravel Runs:
select top 100 * from task_view
select id, name from task_view where active = ? and student_id in (?, ?, ?)
select id, name from task_view where active = ? and teacher_id in (1 ,2 ,3)
Relationships on Model:
public function studentTasks()
{
return $this->hasMany(StudentTasks::class, 'student_id', 'id');
}
public function teacherTasks()
{
return $this->hasMany(TeacherTasks::class, 'teacher_id', 'teacher_id');
}
Calling Code:
TaskView::query()->with(['studentTasks', 'teacherTasks']);
Additional Points:
I think it may have to do with that where the localkey of the relationship (the 3rd argument) is 'id' then the values aren't bound.
My assumption is that bindings are to prevent sql injection and the Docs seem to confirm that. If that's the case then why would id's of the model that the relationship is on not need to be bound? I would assume there's still an issue of SQL Injection there.
I have not seen anyone discussing this from my searching around, (Stackoverflow, Laracasts, Laravel docs)
(I printed out the queries using the below code in AppServiceProvider:boot)
$counter = 0;
\DB::listen(function ($query) use (&$counter) {
echo 'count: '.++$counter.PHP_EOL;
// echo memory_get_usage();
echo $query->sql.PHP_EOL;
echo implode(',', $query->bindings).PHP_EOL;
});
This is a change introduced into Laravel 5.7.14. The initial pull request can be found here. From there you can find more pull requests making updates to the functionality.
It was done as a performance enhancement when needing to eager load a large number of records (many thousands). Instead of having thousands of bound parameters, it puts the raw ids directly in the query. Initially it was done to work around a MySQL PDO bug, but really all database drivers can benefit with not having thousands of bound parameters.
The reason why it does not introduce a SQL injection vulnerability is that:
It only replaces the bindings with the raw values when the ids are integers, and
It runs all the ids through an integer conversion before adding them to the query.
This is the function that ultimately determines if parameters will be used or if raw ids will be used (https://github.com/laravel/framework/blob/7.x/src/Illuminate/Database/Eloquent/Relations/Relation.php#L310-L323):
/**
* Get the name of the "where in" method for eager loading.
*
* #param \Illuminate\Database\Eloquent\Model $model
* #param string $key
* #return string
*/
protected function whereInMethod(Model $model, $key)
{
return $model->getKeyName() === last(explode('.', $key))
&& in_array($model->getKeyType(), ['int', 'integer'])
? 'whereIntegerInRaw'
: 'whereIn';
}
And here is the whereIntegerInRaw() function that shows the keys are int cast before being added into the raw query (https://github.com/laravel/framework/blob/7.x/src/Illuminate/Database/Query/Builder.php#L961-L985):
/**
* Add a "where in raw" clause for integer values to the query.
*
* #param string $column
* #param \Illuminate\Contracts\Support\Arrayable|array $values
* #param string $boolean
* #param bool $not
* #return $this
*/
public function whereIntegerInRaw($column, $values, $boolean = 'and', $not = false)
{
$type = $not ? 'NotInRaw' : 'InRaw';
if ($values instanceof Arrayable) {
$values = $values->toArray();
}
foreach ($values as &$value) {
$value = (int) $value;
}
$this->wheres[] = compact('type', 'column', 'values', 'boolean');
return $this;
}
I've been reading up about the best approach to handling localised times, when a Laravel application is used across multiple timezones.
My understanding is that the app timezone should remain set as the default, which is UTC.
This means that all datetime / timestamps are recorded in the database (MySQL in my case) as their UTC value - in other words, consistently.
For Eloquent models to have the correct (localised) date / time values, the user's timezone must be obeyed. It is at this point that I am less clear on how to proceed - specifically, in terms of:
How the user's timezone is best obtained
How this timezone can be used in a transparent way with Eloquent, so that
All model dates are output in local time
All dates are recorded in the database correctly (as UTC)
Edit
I should mention that my app supports both anonymous and authenticated users, so I don't want to force the user to explicitly select their timezone.
I ended up implementing this with my own model trait, primarily because I needed to implement this in a transparent way.
Firstly, I created my own getAttribute() method, to retrieve the stored values (stored as the app's default timezone - likely UTC) and then apply the current timezone.
The trait also alters the model's create() and update() methods, to support fields in a model's dates property being stored as the app's timezone, when they've been set by the user in the current active timezone.
The self::getLocale() static method in the trait in my case is provided by another trait in my app, although this logic can be adjusted to suit your own app.
trait LocalTime
{
/**
* Override create() to save user supplied dates as app timezone
*
* #param array $attributes
* #param bool|mixed $allow_empty_translations
*/
public static function create(array $attributes = [], $allow_empty_translations=false)
{
// get empty model so we can access properties (like table name and fillable fields) that really should be static!
// https://github.com/laravel/framework/issues/1436
$emptyModel = new static;
// ensure dates are stored with the app's timezone
foreach ($attributes as $attribute_name => $attribute_value) {
// do we have date value, that isn't Carbon instance? (assumption with Carbon is timezone value will be correct)
if (!empty($attribute_value) && !$attribute_value instanceof Carbon && in_array($attribute_name, $emptyModel->dates)) {
// update attribute to Carbon instance, created with current timezone and converted to app timezone
$attributes[$attribute_name] = Carbon::parse($attribute_value, self::getLocale()->timezone)->setTimezone(config('app.timezone'));
}
}
// https://github.com/laravel/framework/issues/17876#issuecomment-279026028
$model = static::query()->create($attributes);
return $model;
}
/**
* Override update(), to save user supplied dates as app timezone
*
* #param array $attributes
* #param array $options
*/
public function update(array $attributes = [], array $options = [])
{
// ensure dates are stored with the app's timezone
foreach ($attributes as $attribute_name => $attribute_value) {
// do we have date value, that isn't Carbon instance? (assumption with Carbon is timezone value will be correct)
if (!empty($attribute_value) && !$attribute_value instanceof Carbon && in_array($attribute_name, $this->dates)) {
// update attribute to Carbon instance, created with current timezone and converted to app timezone
$attributes[$attribute_name] = Carbon::parse($attribute_value, self::getLocale()->timezone)->setTimezone(config('app.timezone'));
}
}
// update model
return parent::update($attributes, $options);
}
/**
* Override getAttribute() to get times in local time
*
* #param mixed $key
*/
public function getAttribute($key)
{
$attribute = parent::getAttribute($key);
// we apply current timezone to any timestamp / datetime columns (these are Carbon objects)
if ($attribute instanceof Carbon) {
$attribute->tz(self::getLocale()->timezone);
}
return $attribute;
}
}
I'd be interested in feedback of the above approach.
I like to add helper function just for that. I keep the app in UTC so all dates are stored the same way ... and then I pass them through the helper either in the controller or in my blade template.
public static function date($date, $format = 'n/j/Y g:i a T'){
$timezone = empty(Auth::user()->timezone) ? 'America/New_York' : Auth::user()->timezone;
if( empty($date) ){
return '--';
}
return Carbon::parse($date)->timezone($timezone)->format($format);
}
This uses a timezone field that was added to the user model -- but will default to Eastern Time if they are a guest. (Eastern just because that's were the business and majority of clients are located.)
I am trying to 'conquer' Laravel, but some concepts still elude me.
Lets take this code:
public function updateMenuGroup($groupId, $label, $icon, $sort, $userId)
{
/*get model to be updated by id*/
$updateGroups = MenuGroups::findMany($groupId);
/**
* set attributes to be updated
* I loop, since I get collection of models fetched by findMany()
*/
foreach($updateGroups as $update)
{
/**
* label (group name) is passed as an array of group names
* for every language, so I am fetching them by using
* language passed by model fetched by findMany (find gets only first mode)
*/
$update->label = $label[$update->lang]; //array
$update->icon = $icon;
$update->sort = $sort;
$update->system_employee_id = $userId;
$update->save();
}
}
$update->label carries an array, with different values for different language - say I have 'en', 'de' languages.
When I run code above, only 'en' is being written - overwriting values of 'de'.
Can someone, please, give me some pointers, as to why above happens?
You need to instantiate update model class inside loop.
foreach($updateGroups as $update)
{
/**
* label (group name) is passed as an array of group names
* for every language, so I am fetching them by using
* language passed by model fetched by findMany (find gets only first mode)
*/
$update=New ModelClass();
$update->label = $label[$update->lang]; //array
$update->icon = $icon;
$update->sort = $sort;
$update->system_employee_id = $userId;
$update->save();
}
You can't insert array to one attribute.
Try to use implode function.
like this.
$update->label = implode(",", $label[$update->lang]);
I have items and units table that have many to many relationship. In other words, the item has many units and the unit has many items. I managed the relation through a junction table item_units. The junction table has some extra field more than item_id and unit_id, i.e it has price, and weight (it is an integer to manage the order of units for each item for display purposes).
I managed the relations in the models as follows:
//In Items model
/**
* #return \yii\db\ActiveQuery
*/
public function getItemUnits()
{
return $this->hasMany(ItemUnits::className(), ['item_id' => 'id'])->orderBy(['item_units.weight' => SORT_DESC]);
}
public function getUnits()
{
return $this->hasMany(Units::className(), ['id'=> 'unit_id'])->select(['id','title'])->via('itemUnits');
}
//
//In Units model
public function getItemUnits()
{
return $this->hasMany(ItemUnits::className(), ['unit_id' => 'id'])->orderBy(['price' => SORT_DESC]);
}
/**
* #return \yii\db\ActiveQuery
*/
public function getItems()
{
return $this->hasMany(Items::className(), ['id' => 'item_id'])->via('itemUnits');
}
//
//In ItemUnits model
public function getItem()
{
return $this->hasOne(Items::className(), ['id' => 'item_id']);
}
/**
* #return \yii\db\ActiveQuery
*/
public function getUnit()
{
return $this->hasOne(Units::className(), ['id' => 'unit_id']);
}
In the controller I'm able to get the data of all related units to an item by something like the following:
$item = Items::findOne($id);
return Json::encode($item->units);
The following is a demo of the JSON object obtained:
[{"id":"4","title":"قرص"},{"id":"5","title":"شريط 10"},{"id":"6","title":"علبة 2 شريط"}]
However, I could not able to order the results according to the weight field in item_units table and also I could not able to include the price field there in the demo result above -JSON Object-.
I only able to get data in item_units as a separate result like the following:
return Json::encode($item->itemUnits);
Update
According to the two answers (#Александр Шалаев & #Onedev.Link) , I have overridden the fields method in Units model as follows:
public function fields() {
parent::fields();
return [
'price' => function($model){
return $model->id; //Here I could not able to get the corresponding price field value from item_units -junction table-
},
'id',
'title',
];
}
However, I could not able to get the price field value from the junction table, temporary, I set it to current model id to prevent error generation. Also, I still has no any mean to set order by using weight field in that junction table.
Update 2
In other words, how could Yii2 Activerecords perform the following SQL query:
SELECT units.id UnitID, units.title Unit, iu.weight, iu.price
FROM units
Left JOIN item_units AS iu
ON iu.item_id = 1 AND iu.unit_id = units.id
WHERE
units.id = iu.unit_id
ORDER BY iu.weight;
Finally I have found a solution. It depends on findBySql method. I'm going to use the above SQL query regarded in Update 2 -just I have removed some selected fields to be suitable for my current task-.
public function actionUnitsJson($id){
$sql = 'SELECT units.id, units.title
FROM units
Left JOIN item_units AS iu
ON iu.item_id = :id AND iu.unit_id = units.id
WHERE
units.id = iu.unit_id
ORDER BY iu.weight DESC;';
$units = \common\models\Units::findBySql($sql,[':id' => $id])->asArray()->all();
return Json::encode($units);
}
You need fields or extraFields in your ActiveRecord model with asArray.
Example:
/**
* #return array
*/
public function fields()
{
return [
'itemUnit', //will get getItemUnit method
];
}
or
/**
* #return array
*/
public function extraFields()
{
return [
'itemUnits', //it is relation name
];
}
Usage:
$model->toArray(); //will contains fields and extra fields relations
... sort array & return
By default, yii\base\Model::fields() returns all model attributes as fields, while yii\db\ActiveRecord::fields() only returns the attributes which have been populated from DB.
You can override fields() to add, remove, rename or redefine fields. The return value of fields() should be an array. The array keys are the field names, and the array values are the corresponding field definitions which can be either property/attribute names or anonymous functions returning the corresponding field values.
Whenever I insert an entity that already exists in the database, I get an error because there is a unique constraint on one of the fields (email).
So I want to check if it already exists; if not, I insert it.
My code looks like this:
$q = Doctrine_Query::create()
->from('User u')
->where('u.email = ?', $email);
$object = $q->fetchOne();
if( ! is_object($object)) {
$user = new User();
$user-email = $email;
$user->save();
}
Is there an easier way to do this?
Put the code you have into a method in your UserTable class eg insertIfNotExists():
public function insertIfNotExists(User $user)
{
// Check if it exists first
$q = self::create("u")
->where("u.email = ?", $user->email)
->execute();
// Do we have any?
if ($q->count())
{
// Yes, return the existing one
return $q->getFirst();
}
// No, save and return the newly created one
$user->save();
return $user;
}
Now you can call the method, and the object returned will be the existing record (if there is one), or the one you've just created.
I was faced with a similar problem while building a database-backed logger. To prevent warning fatigue, I assign each log message a UID which is a hash of its identifying content and made the UID a unique key.
Naturally, this requires that I determine whether a record already exists that matches that UID value (in my case, I increment a count value for that log record and touch its updated_at timestamp).
I ended up overriding Doctrine_Record::save() in my model class, similarly to this (code adjusted to be more relevant to your situation):
/** Persists the changes made to this object and its relations into the
* database.
*
* #param $conn Doctrine_Connection
* #return void
*/
public function save( Doctrine_Connection $conn = null )
{
/* Invoke pre-save hooks. */
$this->invokeSaveHooks('pre', 'save');
/* Check to see if a duplicate object already exists. */
if( $existing = $this->getTable()->findDuplicate($this) )
{
/* Handle duplicate. In this case, we will return without saving. */
return;
}
parent::save($conn);
}
UserTable::findDuplicate() looks like this:
/** Locates an existing record that matches the specified user's email (but
* without matching its PK value, if applicable).
*
* #param $user User
*
* #return User|bool
*/
public function findDuplicate( User $user )
{
$q =
$this->createQuery('u')
->andWhere('u.email = ?', $user->email)
->limit(1);
if( $user->exists() )
{
$q->andWhere('u.id != ?', $user->id);
}
return $q->fetchOne();
}
Note that it is probably a better approach to overwrite preSave() rather than save() in your model. In my case, I had to wait until the pre-save hooks executed (the UID was set using a Doctrine template that I had created), so I had to overwrite save() instead.
You should use Memcached or Redis queue in order to check if item exists or not.