DebugKit Error: CakePHP get() mystery empty elements in associations - cakephp

EDIT: This is after upgrading from CakePHP 4.0 to 4.3
With DebugKit turned on, CakePHP is throwing the following error message:
Argument 1 passed to Cake\ORM\Entity::get() must be of the type string, bool given, called in /Users/thomasbelknap/one-vision/vendor/cakephp/cakephp/src/Datasource/EntityTrait.php on line 607
This appears to be as a result of an empty, numerically-indexed element in the return array of a database query. Let me explain:
In my system, a User has many Estimates (and also Carts and Orders, but more on that). These two clauses in the User and Estimate table files bare this out:
$this->belongsTo('Users', [
'foreignKey' => 'user_id',
]);
::snip::
$this->hasMany('Estimates', [
'foreignKey' => 'user_id',
'dependent' => true,
]);
I am querying this information on a "Dashboard" page using the following query structure:
$user = $this->Users->get($this->getRequest()->getSession()->read('Auth.id'), [
'contain' => [
'UserGroups',
'Carts' => function ($q) {
return $q->limit(5);
},
'Orders' => function ($q) {
return $q->limit(5);
},
'Orders.OrderStatuses',
'Estimates' => function ($q) {
return $q->limit(5);
},
'Estimates.EstimateStatuses',
],
]);
I am not doing any further transformation of the data. There are no Collection functions being employed here. Nevertheless, in the estimates returned, there is always a mystery, numerically-indexed and empty array element:
[1] => Visualize\Model\Entity\Estimate Object
(
[id] => 30
[user_id] => 1
[estimate_status_id] => 5
[estimate_po] => lksjdf
[estimate_total] =>
[name] => Full throttle
[instore_date] => Cake\I18n\FrozenDate Object
(
[date] => 2022-12-31 00:00:00.000000
[timezone_type] => 3
[timezone] => EST
)
[notes] => Let's do this.
[created] => Cake\I18n\FrozenTime Object
(
[date] => 2022-04-19 13:20:39.000000
[timezone_type] => 3
[timezone] => EST
)
[modified] => Cake\I18n\FrozenTime Object
(
[date] => 2022-04-19 13:21:26.000000
[timezone_type] => 3
[timezone] => EST
)
[1] =>
[location_info] => Array
(
)
[[new]] =>
[[accessible]] => Array
(
[user_id] => 1
[estimate_status_id] => 1
[estimate_po] => 1
[estimate_total] => 1
[name] => 1
[instore_date] => 1
[notes] => 1
[created] => 1
[modified] => 1
[user] => 1
[line_items] => 1
[orders] => 1
[estimate_options] => 1
)
[[dirty]] => Array
(
)
[[original]] => Array
(
)
[[virtual]] => Array
(
[item_count] => 1
[box_count] => 1
[deliverable_count] => 1
[0] => location_info
[estimate_total] => 1
)
[[hasErrors]] =>
[[errors]] => Array
(
)
[[invalid]] => Array
(
)
[[repository]] => Estimates
)
This only affects the Estimates part of the query! The other two elements do not have this mystery index. Here's what CakePHP ultimately uses as it's query, which again, seems fine:
SELECT
Estimates.id AS Estimates__id,
Estimates.user_id AS Estimates__user_id,
Estimates.estimate_status_id AS Estimates__estimate_status_id,
Estimates.estimate_po AS Estimates__estimate_po,
Estimates.estimate_total AS Estimates__estimate_total,
Estimates.name AS Estimates__name,
Estimates.instore_date AS Estimates__instore_date,
Estimates.notes AS Estimates__notes,
Estimates.created AS Estimates__created,
Estimates.modified AS Estimates__modified,
EstimateStatuses.id AS EstimateStatuses__id,
EstimateStatuses.name AS EstimateStatuses__name,
EstimateStatuses.description AS EstimateStatuses__description,
EstimateStatuses.created AS EstimateStatuses__created,
EstimateStatuses.modified AS EstimateStatuses__modified
FROM
estimates Estimates
LEFT JOIN estimate_statuses EstimateStatuses ON EstimateStatuses.id = Estimates.estimate_status_id
WHERE
Estimates.user_id in (1)
ORDER BY
Estimates.modified DESC
LIMIT
5
For obvious reasons, I really need the DebugKit to work, but I have no idea where this mystery element is coming from. Everything seems right, to me?
Edit: Adding the definition of my estimate entity:
<?php
declare(strict_types=1);
namespace Visualize\Model\Entity;
use Cake\ORM\Entity;
/**
* Estimate Entity
*
* #property int $id
* #property int $user_id
* #property int|null $estimate_status_id
* #property string|null $estimate_po
* #property float|null $estimate_total
* #property string $name
* #property \Cake\I18n\FrozenDate $instore_date
* #property string $notes
* #property \Cake\I18n\FrozenTime $created
* #property \Cake\I18n\FrozenTime $modified
*
* #property \Visualize\Model\Entity\User $user
* #property \Visualize\Model\Entity\LineItem[] $line_items
* #property \Visualize\Model\Entity\Order[] $orders
* #property \Visualize\Model\Entity\EstimateOption[] $estimate_options
* #property float $item_count
* #property int $box_count
* #property array|\Cake\Collection\CollectionInterface|null $by_location
* #property int $deliverable_count
* #property array $location_info
* #property \Visualize\Model\Entity\EstimateStatus|null $estimate_status
* #property array|\Cake\Collection\CollectionInterface|null $by_deliverable
*/
class Estimate extends Entity
{
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
* Note that when '*' is set to true, this allows all unspecified fields to
* be mass assigned. For security purposes, it is advised to set '*' to false
* (or remove it), and explicitly make individual fields accessible as needed.
*
* #var array
*/
protected $_accessible = [
'user_id' => true,
'estimate_status_id' => true,
'estimate_po' => true,
'estimate_total' => true,
'name' => true,
'instore_date' => true,
'notes' => true,
'created' => true,
'modified' => true,
'user' => true,
'line_items' => true,
'orders' => true,
'estimate_options' => true,
];
/**
* #var array
*/
protected $_virtual = [
'item_count' => true,
'box_count' => true,
'deliverable_count' => true,
'location_info' => true,
'estimate_total' => true,
];
/**
* Returns a zero-filled version of the cannonical ID
*
* #param int $id The autoincremental ID
* #return string
*/
protected function _getId($id): string
{
return sprintf('%05d', $id);
}
/**
* Returns the sum of quantities of all line items.
*
* #return float
*/
protected function _getItemCount(): float
{
$count = 0;
if (!empty($this->line_items)) {
$collection = new \Cake\Collection\Collection($this->line_items);
$count = $collection->sumOf('quantity');
}
return (float)$count;
}
/**
* Returns the count of locations in the estimate
*
* #return int
*/
protected function _getBoxCount(): int
{
$count = 0;
if (!empty($this->line_items)) {
$collection = new \Cake\Collection\Collection($this->line_items);
$collection = $collection->combine('location_id', 'id');
$count = $collection->count();
}
return $count;
}
/**
* Returns the total number of deliverable types in the order.
*
* #return int
*/
protected function _getDeliverableCount(): int
{
$count = 0;
if (!empty($this->line_items)) {
$collection = new \Cake\Collection\Collection($this->line_items);
$collection = $collection->combine('deliverable_id', 'id');
$count = $collection->count();
}
return $count;
}
/**
* Returns the list of locations for this estimate
*
* #return array
*/
protected function _getLocationInfo(): array
{
$info = [];
if (!empty($this->line_items)) {
$collection = new \Cake\Collection\Collection($this->line_items);
$info = $collection->combine(
'location_id',
function ($entity) {
return $entity->location;
}
);
return $info->toArray();
}
return $info;
}
/**
* Returns either the name or the id of the given estimate
*
* #return string
*/
protected function _getName(): string
{
return $this->name ?? (string)$this->id;
}
public const FIELD_ID = 'id';
public const FIELD_USER_ID = 'user_id';
public const FIELD_ESTIMATE_STATUS_ID = 'estimate_status_id';
public const FIELD_ESTIMATE_PO = 'estimate_po';
public const FIELD_ESTIMATE_TOTAL = 'estimate_total';
public const FIELD_NAME = 'name';
public const FIELD_INSTORE_DATE = 'instore_date';
public const FIELD_NOTES = 'notes';
public const FIELD_CREATED = 'created';
public const FIELD_MODIFIED = 'modified';
public const FIELD_USER = 'user';
public const FIELD_LINE_ITEMS = 'line_items';
public const FIELD_ORDERS = 'orders';
public const FIELD_ESTIMATE_OPTIONS = 'estimate_options';
public const FIELD_ITEM_COUNT = 'item_count';
public const FIELD_BOX_COUNT = 'box_count';
public const FIELD_BY_LOCATION = 'by_location';
public const FIELD_DELIVERABLE_COUNT = 'deliverable_count';
public const FIELD_LOCATION_INFO = 'location_info';
public const FIELD_ESTIMATE_STATUS = 'estimate_status';
public const FIELD_BY_DELIVERABLE = 'by_deliverable';
}

Take a close look at the dump of the entity, that field is apparently configured as an exposed virtual field, and looking at the other exposed fields, they seem to be configured incorrectly, as that property should hold a flat array of strings, not string indexed entries like 'item_count' => 1, as the value declares the field name, hence why you see a field named 1.
You are also misinterpreting the output, location_info does not belong to the 1 field, that's actually two different fields, 1 which holds something empty-ish that PHP doesn't print, like null or false, and location_info which holds an empty array.
Use debug() instead of pr()/print_r() to get a better structured dump which also properly shows empty data. And then inspect your Estimate entity class (or wherever things may be configured on the fly) and fix up the virtual fields.

Related

BelongsToMany retrieving and deleting correctly, not updating/saving in CakePHP 3.x

CakePHP 3.8.13 - I have a table model which CAN retrieve the related data from a table and remove it, but when updating/adding an entry, it does not change this data for some reason. So it seems like a correct setup, yet it does not update/add them. In short, it's a "Questions" table and a "Divisions" table, with a relation table called "questions_divisions". In this relation table there is a question_id and a division_id column. If I manually add or remove entries there, it all seems OK. Just when doing patchEntity and save, it does not do it. This is the post data:
'name' => string 'Some name' (length=8)
'questiontype_id' => string '1' (length=1)
'extra' => string '' (length=0)
'answer' => string '' (length=0)
'datafield' => string 'Person.firstname' (length=16)
'divisions' =>
array (size=1)
'_ids' =>
array (size=3)
0 => string '1' (length=1)
1 => string '7' (length=1)
2 => string '5' (length=1)
This is the code that is executed (so the above post data, is $this->request->getData()):
$question = $this->Questions->patchEntity($question, $this->request->getData());
if ($this->Questions->save($question)) {
All this is more or less the same that's happening elsewhere in the application for a different model, which actually DOES add/update the divisions. I also cleared the /tmp/cache just in case, didn't seem to do the trick. I'm at a loss here.
This is the whole table model:
<?php
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
/**
* Questions Model
*
* #property \App\Model\Table\QuestiontypesTable|\Cake\ORM\Association\BelongsTo $Questiontypes
* #property \App\Model\Table\ActivitiesTable|\Cake\ORM\Association\BelongsToMany $Activities
* #property \App\Model\Table\BookingformsTable|\Cake\ORM\Association\BelongsToMany $Bookingforms
* #property \App\Model\Table\DivisionsTable|\Cake\ORM\Association\BelongsToMany $Divisions
*
* #method \App\Model\Entity\Question get($primaryKey, $options = [])
* #method \App\Model\Entity\Question newEntity($data = null, array $options = [])
* #method \App\Model\Entity\Question[] newEntities(array $data, array $options = [])
* #method \App\Model\Entity\Question|bool save(\Cake\Datasource\EntityInterface $entity, $options = [])
* #method \App\Model\Entity\Question|bool saveOrFail(\Cake\Datasource\EntityInterface $entity, $options = [])
* #method \App\Model\Entity\Question patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
* #method \App\Model\Entity\Question[] patchEntities($entities, array $data, array $options = [])
* #method \App\Model\Entity\Question findOrCreate($search, callable $callback = null, $options = [])
*/
class QuestionsTable extends Table {
public function beforeFind ($event, $query, $options, $primary) {
$order = $query->clause('order');
if ($order === null || !count($order)) {
$query->order([$this->getAlias() . '.name' => 'ASC']);
}
}
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config) {
parent::initialize($config);
$this->setTable('questions');
$this->setDisplayField('name');
$this->setPrimaryKey('id');
$this->addBehavior('Search.Search');
$this->addBehavior('Page');
$this->hasMany('Questions_divisions', [
'foreignKey' => 'question_id'
]);
$this->belongsTo('Questiontypes', [
'foreignKey' => 'questiontype_id',
'joinType' => 'INNER'
]);
$this->belongsToMany('Activities', [
'foreignKey' => 'question_id',
'targetForeignKey' => 'activity_id',
'joinTable' => 'activities_questions'
]);
$this->belongsToMany('Bookingforms', [
'foreignKey' => 'questions_id',
'targetForeignKey' => 'bookingforms_id',
'joinTable' => 'questions_bookingforms'
]);
$this->belongsToMany('Divisions', [
'foreignKey' => 'question_id',
'targetForeignKey' => 'division_id',
'joinTable' => 'questions_divisions'
]);
//Setup searchfield
$this->behaviors()->Page->searchSetup($this->behaviors()->Search->searchManager(), $this->searchForm());
}
/**
* Default validation rules.
*
* #param \Cake\Validation\Validator $validator Validator instance.
* #return \Cake\Validation\Validator
*/
public function validationDefault(Validator $validator) {
$validator
->integer('id')
->allowEmpty('id', 'create');
$validator
->scalar('name')
->maxLength('name', 500)
->requirePresence('name', 'create')
->notEmpty('name');
$validator
->scalar('extra')
->maxLength('extra', 500)
->allowEmpty('extra');
$validator
->scalar('answer')
->maxLength('answer', 1500)
->allowEmpty('answer');
$validator
->scalar('datafield')
->maxLength('datafield', 500)
->requirePresence('datafield', 'create')
->notEmpty('datafield');
return $validator;
}
/**
* Returns a rules checker object that will be used for validating
* application integrity.
*
* #param \Cake\ORM\RulesChecker $rules The rules object to be modified.
* #return \Cake\ORM\RulesChecker
*/
public function buildRules(RulesChecker $rules) {
$rules->add($rules->existsIn(['questiontype_id'], 'Questiontypes'));
return $rules;
}
/**
* Returns an array which describes the search form
*
* #return Array with search setup
*/
public function searchForm() {
//Set up search form
$search_data = [['id' => 'name', 'label' => 'Naam', 'type' => 'textfield', 'fields' => ['name']],
['id' => 'Questiontypes_id', 'label' => 'Vraagtype', 'type' => 'multiselect', 'fields' => ['Questiontypes.id'], 'options' => $this->Questiontypes->find('list')],
['id' => 'datafield', 'label' => 'Dataveld', 'type' => 'textfield', 'fields' => ['datafield']]
];
return $search_data;
}
}
Seems like adding this column in the $_accessible array (inside the Model/Entity) does the trick... That's why it could retrieve the data, but not edit it - hence "accessible" maybe? Not sure if that's the correct name for it but hey..
protected $_accessible = [
'name' => true,
'extra' => true,
'answer' => true,
'datafield' => true,
'questiontype_id' => true,
'questiontype' => true,
'activities' => true,
'bookingforms' => true,
'divisions' => true // this did the trick!
];

Yii 2.0 - Trying to get property of non-object

I try to change the "ID Kategori"(Category ID) to "Nama Kategori" (Category Name), the Category ID is in product table and has a relation to category table.
for the gridView im using kartik-v gridView
i know the error is at return Html::a($model->kategori->deskripsi ,['kategori/view','id' => $model->Id]);, But i dont know whats the problem or how to fix it
please help me... >.<
[
'label' => 'Kategori',
'attribute' => 'IdKategori',
'format' => 'raw',
'vAlign' => 'middle',
'value' => function ($model, $key, $index) {
return Html::a($model->kategori->deskripsi ,['kategori/view','id' => $model->Id]);
},
],
and this is the model
<?php
namespace common\models;
use Yii;
/**
* This is the model class for table "produk".
*
* #property integer $Id
* #property integer $IdKategori
* #property string $nama_produk
* #property integer $harga_produk
* #property string $gambar
* #property string $deksripsi_produk
* #property string $detail_produk
*/
class Produk extends \yii\db\ActiveRecord
{
/**
* #inheritdoc
*/
public $file;
public static function tableName()
{
return 'produk';
}
/**
* #inheritdoc
*/
public function rules()
{
return [
[['IdKategori', 'nama_produk', 'harga_produk', 'gambar', 'deksripsi_produk', 'detail_produk'], 'required'],
[['IdKategori', 'harga_produk'], 'integer'],
[['file'], 'file'],
[['nama_produk', 'file', 'gambar', 'deksripsi_produk', 'detail_produk'], 'string', 'max' => 255],
[['IdKategori'], 'exist', 'skipOnError' => true, 'targetClass' => Kategori::className(), 'targetAttribute' => ['IdKategori' => 'Id']],
];
}
/**
* #inheritdoc
*/
public function attributeLabels()
{
return [
'Id' => 'ID',
'IdKategori' => 'Id Kategori',
'nama_produk' => 'Nama Produk',
'harga_produk' => 'Harga Produk',
'gambar' => 'Gambar',
'deksripsi_produk' => 'Deksripsi Produk',
'detail_produk' => 'Detail Produk',
];
}
public function getKategori()
{
return $this->hasOne(Kategori::className(), ['Id' => 'IdKategori']);
}
}
This is the kategori model
<?php
namespace common\models;
use Yii;
/**
* This is the model class for table "kategori".
*
* #property integer $Id
* #property integer $ParentId
* #property string $nama_kategori
* #property string $deskripsi
*/
class Kategori extends \yii\db\ActiveRecord
{
/**
* #inheritdoc
*/
public static function tableName()
{
return 'kategori';
}
/**
* #inheritdoc
*/
public function rules()
{
return [
[['ParentId', 'nama_kategori', 'deskripsi'], 'required'],
[['ParentId'], 'integer'],
[['nama_kategori', 'deskripsi'], 'string', 'max' => 255],
];
}
/**
* #inheritdoc
*/
public function attributeLabels()
{
return [
'Id' => 'ID',
'ParentId' => 'Parent ID',
'nama_kategori' => 'Nama Kategori',
'deskripsi' => 'Deskripsi',
];
}
}
I checked myself to make sure I can repeat your error and it seems to be the case.
In your database, table produkt has a column called IdKategori and at least 1 row contains either null or non-existing value for table kategori (non-existing value means that it has an ID that does not exist in kategori table).
You can solve that with (one example):
'value' => function ($model, $key, $index) {
if (empty($model->kategori->deskripsi)) {
return '';
} else {
return Html::a($model->kategori->deskripsi, ['kategori/view', 'id' => $model->Id]);
}
},

Symfony 2 override entity field property

I want to override the entity field property. I need to get data from another database table (mapped by id). It should be a combination of "artikelnummer" and a field called "name" from another database table.
$builder->add('schlauch', 'entity', array(
'class' => 'SchlauchBundle:Artikelspezifikation',
'property' => 'artikelnummer',
'attr' => array(
'class' => 'extended-select'
),
'data_class' => null
));
The field "artikelnummer" outputs something like "12345" but I need to add the name (from another database table called "schlauch"), so it should look like "12345 Articlename". I tried a query in the entity file, but I dont't want to manipulate the output everywhere.
Is it possible to use a query for property and override it?
You can simple solve that by adding new getter to you entity:
class Artikelspezifikation
{
//…
/**
* #var Schlauch
*
* #ORM\ManyToOne(targetEntity="Schlauch", inversedBy="artikelspezifikations")
*/
private $schlauch;
//…
/**
* Get display name
*
* #return string
*/
public function getDisplayName()
{
return $this->artikelnummer . ' ' . $this->schlauch->getArtikelName();
}
//…
/**
* Set schlauch
*
* #param \SchlauchBundle\Entity\Schlauch $schlauch
*
* #return Artikelspezifikation
*/
public function setCategory(\SchlauchBundle\Entity\Schlauch $schlauch = null)
{
$this->schlauch = $schlauch;
return $this;
}
/**
* Get schlauch
*
* #return \SchlauchBundle\Entity\Schlauch
*/
public function getCategory()
{
return $this->schlauch;
}
}
And in your form class just change property:
$builder->add('schlauch', 'entity', array(
'class' => 'SchlauchBundle:Artikelspezifikation',
'property' => 'displayName',
'attr' => array(
'class' => 'extended-select'
),
'data_class' => null
));

Magento 2 adminhtml multiselect and showing selected options after save

How do I get the Magento2 Adminhtml Form with a Multiselect to select the saved options?
Here is my Adminhtml Form Class for reference.
namespace RussellAlbin\Blog\Block\Adminhtml\Post\Edit;
/**
* Adminhtml blog post edit form
*/
class Form extends \Magento\Backend\Block\Widget\Form\Generic
{
/**
* #var \RussellAlbin\Blog\Model\Category\Source\ListCategories
*/
protected $_categories;
/**
* #var \RussellAlbin\Blog\Model\Postcategory
*/
protected $_postcategory;
/**
* #var \Magento\Store\Model\System\Store
*/
protected $_systemStore;
/**
* #param \Magento\Backend\Block\Template\Context $context
* #param \Magento\Framework\Registry $registry
* #param \Magento\Framework\Data\FormFactory $formFactory
* #param \Magento\Store\Model\System\Store $systemStore
* #param \RussellAlbin\Blog\Model\Category\Source\ListCategories $categories
* #param \RussellAlbin\Blog\Model\Postcategory $postcategory
* #param array $data
*/
public function __construct(
\Magento\Backend\Block\Template\Context $context,
\Magento\Framework\Registry $registry,
\Magento\Framework\Data\FormFactory $formFactory,
\Magento\Store\Model\System\Store $systemStore,
\RussellAlbin\Blog\Model\Category\Source\ListCategories $categories,
\RussellAlbin\Blog\Model\Postcategory $postcategory,
array $data = []
) {
$this->_categories = $categories;
$this->_systemStore = $systemStore;
$this->_postcategory = $postcategory;
parent::__construct($context, $registry, $formFactory, $data);
}
/**
* Init form
*
* #return void
*/
protected function _construct()
{
parent::_construct();
$this->setId('blog_post_form');
$this->setTitle(__('Blog Post Information'));
}
/**
* Prepare form
*
* #return $this
*/
protected function _prepareForm()
{
/** #var \RussellAlbin\Blog\Model\Post $model */
$model = $this->_coreRegistry->registry('blog_post');
/** #var \Magento\Framework\Data\Form $form */
$form = $this->_formFactory->create(
['data' => ['id' => 'edit_form', 'action' => $this->getData('action'), 'method' => 'post']]
);
$form->setHtmlIdPrefix('post_');
$fieldset = $form->addFieldset(
'base_fieldset',
['legend' => __('General Information'), 'class' => 'fieldset-wide']
);
if ($model->getPostId()) {
$fieldset->addField('post_id', 'hidden', ['name' => 'post_id']);
}
// Gather our existing categories
$currentCategories = $this->_getExistingCategories( $model );
// Get all the categories that in the database
$allCategories = $this->_categories->toOptionArray();
$field = $fieldset->addField(
'blog_categories',
'multiselect',
[
'label' => __('Categories'),
'required' => true,
'name' => 'blog_categories',
'values' => $allCategories,
'value' => $currentCategories
]
);
$form->setValues($model->getData());
$form->setUseContainer(true);
$this->setForm($form);
return parent::_prepareForm();
}
/**
* #param $model
* #return array
*/
private function _getExistingCategories( $model )
{
// Get our collection
$existingCategories = $this->_postcategory->getCollection()
->addFieldToSelect('category_id')
->addFieldToFilter('post_id', $model->getId());
// Setup our placeholder for the array of categories needed to set back on the value of the multiselect
$itemList = array();
foreach($existingCategories as $_item)
{
$itemList[] = $_item['category_id'];
}
return $itemList;
}
}
I looked back at some work I have done in Magento 1.x and I the only way I got this to work before was using javascript and the setAfterElementHtml to set the options that matched. I remember hating this method of getting it accomplished because it seemed like a work around.
I found the issue.
I did not have the values saved on the model.
// This is what shows it as selected on reload
$model->setData('blog_categories', $categories);
$fieldset->addField(
'blog_categories',
'multiselect',
[
'name' => 'blog_categories[]',
'label' => __('Categories'),
'title' => __('Categories'),
'required' => true,
'values' => $optionArray,
'disabled' => false
]
);

ZF2 Dafault Database Session Storage

I am busy writing a DB Session module so I can quickly install it between multiple applications. The module will be loaded from the autoloader as the first module to be started. What I'm trying to accomplish is to change the default session container / session handler to be the default session handler for all modules and it should also be database hosted sessions. I've been strugling with zf2 session handler for quite a while now and the errors in the logs make 0 sense. So here what I have so far. A basic module with Module.php containing...
namespace DBSession;
use Zend\Mvc\ModuleRouteListener;
class Module {
public function onBootstrap($e) {
$e->getApplication()->getServiceManager()->get('translator');
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
$config = $e->getApplication()->getServiceManager()->get('Config');
$controller = $e->getTarget();
$controller->config = $config;
new \DBSession\Storage\DBStorage();
}
public function getConfig() {
return include __DIR__ . '/config/module.config.php';
}
public function getAutoloaderConfig() {
return array(
'Zend\Loader\ClassMapAutoloader' => array(
__DIR__ . '/autoload_classmap.php',
),
'Zend\Loader\StandardAutoloader' => array(
'namespaces' => array(
__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
),
),
);
}
}
And the actual class that initiates the DB session handler.
namespace DBSession\Storage;
use Zend\Session\SaveHandler\DbTableGateway;
use Zend\Session\SaveHandler\DbTableGatewayOptions;
use Zend\Db\Adapter\Adapter;
use Zend\Session\SessionManager;
use Zend\Session\Container;
class DBStorage {
public function __construct() {
$dbAdapter = new Adapter(array(
'driver' => 'Mysqli',
'host' => 'localhost',
'dbname' => 'zf2_session',
'username' => 'zf2',
'password' => 'testme',
'options' => array(
'buffer_results' => true,
),
));
$tableGateway = new \Zend\Db\TableGateway\TableGateway('session', $dbAdapter);
$gwOpts = new DbTableGatewayOptions();
$gwOpts->setDataColumn('data');
$gwOpts->setIdColumn('id');
$gwOpts->setLifetimeColumn('lifetime');
$gwOpts->setModifiedColumn('modified');
$gwOpts->setNameColumn('name');
$saveHandler = new DbTableGateway($tableGateway, $gwOpts);
$sessionManager = new SessionManager();
$sessionManager->setSaveHandler($saveHandler);
return Container::setDefaultManager($sessionManager);
}
}
When trying to create a session I see the following in the logs which I have 0 clue how to fix. This is starting to make me hate magic...
[29-Nov-2012 20:47:28 UTC] PHP Fatal error: Uncaught exception 'Zend\Db\Sql\Exception\InvalidArgumentException' with message 'Not a valid magic property for this object' in /document_root/vendor/zendframework/zendframework/library/Zend/Db/Sql/Select.php:764
Stack trace:
#0 /document_root/vendor/zendframework/zendframework/library/Zend/Db/Sql/Select.php(163): Zend\Db\Sql\Select->__get('tableReadOnly')
#1 /document_root/vendor/zendframework/zendframework/library/Zend/Db/Sql/Select.php(146): Zend\Db\Sql\Select->from('session')
#2 /document_root/vendor/zendframework/zendframework/library/Zend/Db/Sql/Sql.php(65): Zend\Db\Sql\Select->__construct('session')
#3 /document_root/vendor/zendframework/zendframework/library/Zend/Db/TableGateway/AbstractTableGateway.php(191): Zend\Db\Sql\Sql->select()
#4 /document_root/vendor/zendframework/zendframework/library/Zend/Session/SaveHandler/DbTableGateway.php(134): Zend\Db\TableGateway\AbstractTableGateway->select(Array)
#5 [internal function]: Zend\Session\SaveHandler\DbTableGateway->write(' in /document_root/vendor/zendframework/zendframework/library/Zend/Db/Sql/Select.php on line 764
update :
zend framework >= 2.2, this issue is no more .
Old answer :
I have faced same issue on storing session into database , I have written initDbSession function inside basic module class \module\Application\Module.php
class Module
{
public function onBootstrap(MvcEvent $e)
{
$e->getApplication()->getServiceManager()->get('translator');
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
// session start from here
$this->initDbSession( $e );
}
/**
* Store session into database
*
* #param type $e
*/
private function initDbSession( MvcEvent $e )
{
// grab the config array
$serviceManager = $e->getApplication()->getServiceManager();
$config = $serviceManager->get('config');
$dbAdapter = $serviceManager->get('Zend\Db\Adapter\Adapter');
/* some how this not works for me
$sessionOptions = new \Zend\Session\SaveHandler\DbTableGatewayOptions( null );
$sessionTableGateway = new \Zend\Db\TableGateway\TableGateway('session', $dbAdapter);
$saveHandler = new \Zend\Session\SaveHandler\DbTableGateway($sessionTableGateway, $sessionOptions);
*/
/* I written my own save handler , I am using mysql as database */
$saveHandler = new \My\Session\SaveHandler\Mysql( $config['db'] );
$sessionConfig = new \Zend\Session\Config\SessionConfig();
$sessionConfig->setOptions($config['session']);
// pass the saveHandler to the sessionManager and start the session
$sessionManager = new \Zend\Session\SessionManager( $sessionConfig , NULL, $saveHandler );
$sessionManager->start();
\Zend\Session\Container::setDefaultManager($sessionManager);
}
// other function goes here ...
Here my config file , which is located in \config\autoload\global.php
return array(
'db' => array(
'driver' => 'Pdo',
'dsn' => 'mysql:dbname=zf2;host=localhost',
'driver_options' => array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'',
'buffer_results' => true
),
'username' => 'root',
'password' => '',
'host' => 'localhost',
'dbname' => 'zf2',
),
'session' => array(
'remember_me_seconds' => 2419200,
'use_cookies' => true,
'cookie_httponly' => true,
'cookie_lifetime' => 2419200,
'gc_maxlifetime' => 2419200,
),
'service_manager' => array(
'factories' => array(
'Zend\Db\Adapter\Adapter' => 'Zend\Db\Adapter\AdapterServiceFactory'
)
)
);
Mysql Table structure is
CREATE TABLE `session` (
`id` CHAR(32) NOT NULL DEFAULT '',
`name` VARCHAR(255) NOT NULL,
`modified` INT(11) NULL DEFAULT NULL,
`lifetime` INT(11) NULL DEFAULT NULL,
`data` TEXT NULL,
PRIMARY KEY (`id`)
)COLLATE='utf8_general_ci' ENGINE=InnoDB;
Custom Mysql save handler class is given below . I have written this class, because new Zend\Session\SaveHandler\DbTableGateway
not working on my server . My custom Mysql session save handler class written on \library\My\Session\SaveHandler\Mysql.php
<?php
namespace My\Session\SaveHandler;
use Zend\Session\SaveHandler\SaveHandlerInterface;
/**
* Description of Mysql
*
* #author rab
*/
class Mysql implements SaveHandlerInterface
{
/**
* Session Save Path
*
* #var string
*/
protected $sessionSavePath;
/**
* Session Name
*
* #var string
*/
protected $sessionName;
/**
* Lifetime
* #var int
*/
protected $lifetime;
/**
* Constructor
*
*/
public function __construct( $dbConfig )
{
$this->dbconn = mysql_connect(
$dbConfig['host'],
$dbConfig['username'],
$dbConfig['password']
);
if ( $this->dbconn ) {
return mysql_select_db($dbConfig['dbname'], $this->dbconn);
}
}
/**
* Open the session
*
* #return bool
*/
public function open( $savePath, $name )
{
$this->sessionSavePath = $savePath;
$this->sessionName = $name;
$this->lifetime = ini_get('session.gc_maxlifetime');
return true;
}
/**
* Close the session
*
* #return bool
*/
public function close()
{
return mysql_close($this->dbconn);
}
/**
* Read the session
*
* #param int session id
* #return string string of the sessoin
*/
public function read($id)
{
$id = mysql_real_escape_string($id);
$sql = "SELECT `data` FROM `session` " .
"WHERE id = '$id'";
if ( $result = mysql_query($sql, $this->dbconn)) {
if ( mysql_num_rows($result) ) {
$record = mysql_fetch_assoc($result);
return $record['data'];
}
}
return '';
}
/**
* Write the session
*
* #param int session id
* #param string data of the session
*/
public function write($id, $data )
{
$data = (string) $data ;
$dbdata = array(
'modified' => time(),
'data' => mysql_real_escape_string( $data ) ,
);
$selectSql = "SELECT * FROM session
WHERE id = '$id' AND name = '{$this->sessionName}' ";
$rs = mysql_query( $selectSql, $this->dbconn );
if ( $rs = mysql_query( $selectSql , $this->dbconn)) {
if ( mysql_num_rows($rs) ) {
$updateSql = "UPDATE `session` SET
`modified`= '".$dbdata['modified'] . "' ,
`data`= '".$dbdata['data']. "'
WHERE id= '$id' AND name = '{$this->sessionName}' ";
mysql_query( $updateSql , $this->dbconn );
return true;
}
}
$dbdata['lifetime'] = $this->lifetime;
$dbdata['id'] = $id;
$dbdata['name'] = $this->sessionName;
$insertSql = "INSERT INTO session (". implode(',' , array_keys($dbdata)) .")"
."VALUES ('" . implode("','" , array_values( $dbdata )). "')";
return mysql_query( $insertSql, $this->dbconn);
}
/**
* Destoroy the session
*
* #param int session id
* #return bool
*/
public function destroy($id)
{
$sql = sprintf("DELETE FROM `session` WHERE `id` = '%s'", $id);
return mysql_query($sql, $this->dbconn);
}
/**
* Garbage Collector
*
* #param int life time (sec.)
* #return bool
*/
public function gc( $maxlifetime )
{
$sql = sprintf("DELETE FROM `session` WHERE `modified` < '%s'",
mysql_real_escape_string(time() - $maxlifetime)
);
return mysql_query($sql, $this->dbconn);
}
}
Which saves my session values into db table . For more information you can check http://php.net/manual/en/function.session-set-save-handler.php
After all of the fixes that have recently gone into ZF2 my original solution is now working without modification however, I have added a new composer module for anyone that wants to use it, it reduces the need to do it all manually.
Installation details are in the readme file on: https://github.com/Nitecon/DBSessionStorage
Enjoy and let me know if you have any issues.
Here's another similar solution:
How to use cookie in Zend Framework 2?
make sure you change this line:
Session::setDefaultManager($sessionManager);
With this one:
Container::setDefaultManager($sessionManager);
Using:
use Zend\Session\Container;

Resources