Zend Framework - ORM relationships and optimization - zend-framework

I've been using ZF for few months and I'm really happy with it however I'm not completely sure about how to work with models relationships and at the same time avoid multiple queries to the db. Many people has this problem and no one seems to find a good solution for it. (and avoiding using a third party ORM) For example I have a list of users, and each user belongs to a group. I want a list of users displaying user info and group name (to tables: users, and groups. Users has a foreign key to the table groups).
I have:
2 mapper classes to handle those tables, UserMapper and GroupMapper.
2 Model Classes User and Group
2 Data Source classes that extends Zend_DB_Table_Abstract
in user mapper I can do findParentRow in order to get the group info of each user, but the problem is i have an extra query for each row, this is not good I think when with a join I can do it in only one. Of course now we have to map that result to an object. so in my abstract Mapper class I attempt to eager load the joining tables for each parent row using column aliasing (similar as Yii does.. i think) so I get in one query a value object like this
//User model object
$userMapper= new UserMapper();
$users= $userMapper->fetchAll(); //Array of user objects
echo $user->id;
echo $user->getGroup()->name // $user->getParentModel('group')->name // this info is already in the object so no extra query is required.
I think you get my point... Is there a native solution, perhaps more academic than mine, in order to do this without avoiding multiple queries? // Zend db table performs extra queries to get the metadata thats ok and can be cached. My problem is in order to get the parent row info... like in yii.... something like that $userModel->with('group')->fetchAll();
Thank you.

Develop your mapper to work with Zend_Db_Select. That should allow for flexibility you need. Whether group table is joined depends on the parameter provided to mapper methods, in this example group object is the critical parameter.
class Model_User {
//other fields id, username etc.
//...
/**
* #var Model_Group
*/
protected $_group;
public function getGroup() {
return $this->_group;
}
public function setGroup(Model_Group $group) {
$this->_group = $group;
}
}
class Model_Mapper_User {
/**
* User db select object, joins with group table if group model provided
* #param Model_Group $group
* #return Zend_Db_Select
*/
public function getQuery(Model_Group $group = NULL) {
$userTable = $this->getDbTable('user'); //mapper is provided with the user table
$userTableName = $userTable->info(Zend_Db_Table::NAME); //needed for aliasing
$adapter = $userTable->getAdapter();
$select = $adapter->select()->from(array('u' => $userTableName));
if (NULL !== $group) {
//group model provided, include group in query
$groupTable = $this->getDbTable('group');
$groupTableName = $groupTable->info(Zend_Db_Table::NAME);
$select->joinLeft(array('g' => $groupTableName),
'g.group_id = u.user_group_id');
}
return $select;
}
/**
* Returns an array of users (user group optional)
* #param Model_User $user
* #param Model_Group $group
* #return array
*/
public function fetchAll(Model_User $user, Model_Group $group = NULL) {
$select = $this->getQuery();
$adapter = $select->getAdapter();
$rows = $adapter->fetchAll($select);
$users = array();
if (NULL === $group) {
foreach ($rows as $row) {
$users[] = $this->_populateUser($row, clone $user);
}
} else {
foreach ($rows as $row) {
$newUser = $this->_populateUser($row, clone $user);
$newGroup = $this->_populateGroup($row, clone $group);
//marrying user and group
$newUser->setGroup($newGroup);
$users[] = $newUser;
}
}
return $users;
}
/**
* Populating user object with data
*/
protected function _populateUser($row, Model_User $user) {
//setting fields like id, username etc
$user->setId($row['user_id']);
return $user;
}
/**
* Populating group object with data
*/
protected function _populateGroup($row, Model_Group $group) {
//setting fields like id, name etc
$group->setId($row['group_id']);
$group->setName($row['group_name']);
return $group;
}
/**
* This method also fits nicely
* #param int $id
* #param Model_User $user
* #param Model_Group $group
*/
public function fetchById($id, Model_User $user, Model_Group $group = NULL) {
$select = $this->getQuery($group)->where('user_id = ?', $id);
$adapter = $select->getAdapter();
$row = $adapter->fetchRow($select);
$this->_populateUser($row, $user);
if (NULL !== $group) {
$this->_populateGroup($row, $group);
$user->setGroup($group);
}
return $user;
}
}
use scenarios
/**
* This method needs users with their group names
*/
public function indexAction() {
$userFactory = new Model_Factory_User();
$groupFactory = new Model_Factory_Group();
$userMapper = $userFactory->createMapper();
$users = $userMapper->fetchAll($userFactory->createUser(),
$groupFactory->createGroup());
}
/**
* This method needs no user group
*/
public function otherAction() {
$userFactory = new Model_Factory_User();
$userMapper = $userFactory->createMapper();
$users = $userMapper->fetchAll($userFactory->createUser());
}
Cheers

I have written a solution by subclassing Zend_Db_Table_Rowset_Abstract and Zend_Db_Table_Row_Abstract. I'll try to summarise it briefly and if it is of interest to any one I can expand on it.
I created an abstract model class - My_Db_Table_Row - that contains an array (keyed on child classname) of rowsets of children.
I created an abstract Rowset class - My_Db_Table_Rowset - that extracts the data from a query based on column names and creates rowsets stored in My_Db_Table_Row_children.
The My_Db_Table_Rowset class uses _dependantTables and _referenceMap from Zend_Db_Table_Abstract to create child instances (from joined columns) and add them to the appropriate array within the _children of their parent instance (created from 'primary table' columns).
Accessing a child is done as follows: $car->getDrivers();
public function getDrivers() {
// allow for lazy loading
if (!isset($this->_children['My_Model_Driver'])) {
$this->_children['My_Model_Driver'] = My_Model_Driver::fetch........;
}
return $this->_children('My_Model_Driver');
}
Initially, I coded this for 2 levels, parent and child but I am in the process of extending this to handle more levels, e.g. grandparent-parent-child.

Related

conversion of magento 1 into magento 2

I was using fetch_assoc() method in magento 1 .
I want to convert it into Magento 2 . there is no fetch_assoc() method in magento 2.
if(is_object($result))
{
while ($resultsArray =$result->fetch_assoc())
{
if(empty($data))
{
$data[] = array_keys($resultsArray);
}
$data[] = $resultsArray;
} var_dump($data);
}
I'm not sure my proposed solution is useful for you or not but the best approach to fetch data in Magento 2 is based on Models and Collections.
Step 1: Firstly, you have to create a Model file in your module
<?php
namespace <Vendor_Name>\<Module_Name>\Model;
use Magento\Framework\Model\AbstractModel;
class Data extends AbstractModel
{
protected function _construct()
{
$this->_init('<Vendor_Name>\<Module_Name>\Model\ResourceModel\Data');
}
}
Step 2: Create ResourceModel file in your custom module
<?php
namespace <Vendor_Name>\<Module_Name>\Model\ResourceModel;
use \Magento\Framework\Model\ResourceModel\Db\AbstractDb;
class Data extends AbstractDb
{
protected function _construct()
{
// Second parameter is a primary key of the table
$this->_init('Table_Name', 'id');
}
}
Step 3: Create Collection file to initialize Model and ResourceModel files.
namespace <Vendor_Name>\<Module_Name>\Model\ResourceModel\Data;
use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;
class Collection extends AbstractCollection
{
protected function _construct()
{
$this->_init(
'<Vendor_Name>\<Module_Name>\Model\Data',
'<Vendor_Name>\<Module_Name>\Model\ResourceModel\Data'
);
}
}
Step 4: Last thing that you need to do is create a Block file in the same module and utilize collection, something like this:
namespace <Vendor_Name>\<Module_Name>\Block;
use Magento\Framework\View\Element\Template\Context;
use Magento\Framework\View\Element\Template;
use <Vendor_Name>\<Module_Name>\Model\Data as DataCollection;
class Custom_Module extends Template
{
protected $dataCollection;
public function __construct(Context $context, DataCollection $dataCollection)
{
$this->_dataCollection = $dataCollection;
parent::__construct($context);
}
public function getDataCollecton()
{
$collection = $this->_dataCollection->getCollection();
return $collection;
}
}
Another Solution
You can also use fetchAll instead of fetch_assoc() in Magento 2, if you don't want to implement models and collections based solution, something like this:
// Select Data from table
$sql = "Select * FROM " . $tableName;
$result = $connection->fetchAll($sql);
and for reference, you can also have a look into Magento2 – Write Custom Mysql Query (Without Using Model)
I think we can use something like below :
$adapter = $this->resourceConnection->getConnection($resource);
$stmt = $adapter->query($sql);
// Use FETCH_NUM so we are not dependent on the CASE attribute of the PDO connection
$results = $stmt->fetchAll(\Zend_Db::FETCH_ASSOC);
Or if we have $connection instanceof \Magento\Framework\DB\Adapter\AdapterInterface
$connection->fetchAll($sql, $binds, \PDO::FETCH_ASSOC);
By using those, i think you're gonna get the same result to magento 1 fetch_assoc
Alternative of the fetch_assoc() in magento 2 is fetchAssoc($SQL_QUERY)
Below is the example.
For get order where status is pending data using fetchAssoc(SQL_QUERY)
<?php
namespace Path\To\Class;
use Magento\Framework\App\ResourceConnection;
class fetchAssoc {
const ORDER_TABLE = 'sales_order';
/**
* #var ResourceConnection
*/
private $resourceConnection;
public function __construct(
ResourceConnection $resourceConnection
) {
$this->resourceConnection = $resourceConnection;
}
/**
* fetchAssoc Sql Query
*
* #return string[]
*/
public function fetchAssocQuery()
{
$connection = $this->resourceConnection->getConnection();
$tableName = $connection->getTableName(self::ORDER_TABLE);
$query = $connection->select()
->from($tableName,['entity_id','status','grand_total'])
->where('status = ?', 'pending');
$fetchData = $connection->fetchAssoc($query);
return $fetchData;
}
}
In magento 2 you can use same but for that you need to create database connection.
I suggest you to use resource or collection models to get the result and if you want to get the first row in object format then you should use
getFirstItem();
I think Magento 2 has support for this in class \Magento\Framework\DB\Adapter\AdapterInterface. You can create an instance for AdapterInterface by dependency injection or directly by object manager.
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
/** #var \Magento\Framework\App\ResourceConnection $resourceConnection */
$resourceConnection = $objectManager->get(\Magento\Framework\App\ResourceConnection::class);
/** #var \Magento\Framework\DB\Adapter\AdapterInterface $connection */
$connection = $resourceConnection->getConnection(\Magento\Framework\App\ResourceConnection::DEFAULT_CONNECTION);
$sql = "YOUR SELECT QUERY HERE";
$data = $connection->fetchAssoc($sql);

Is it possible to disable lazy loading for ODM Doctrine?

We are developing API with Silex and Doctrine (ODM) and we have object Story, which have property images.
class Story extends AbstractDocument
{
/** #MongoDB\Id */
protected $id;
/**
* #MongoDB\ReferenceMany(
* targetDocument="MyNamespace\Documents\Image",
* storeAs="DBRef"
* )
*/
protected $images = [];
// Other properties and methods
}
We have get method in repository (in AbstractRepository, from which extends all other repositories).
public function get(string $documentId) : array
{
$document = $this->createQueryBuilder()
->field('id')->equals($documentId)
->hydrate(false)
->getQuery()
->toArray();
}
This method returns embedded and referenced objects, but for referenceMany returns only ids without data.
Is it possible to deny lazy loading to get all documents ?
One possible solution, which we found - rewrite method toArray.
As soon as you use ->hydrate(false) you are instructing ODM to get out of your way and return you raw data from MongoDB. You are seeing the referenceMany as an array of ids because that is the raw representation, no lazy loading is involved.
The cleanest way to solve your issue would be implementing StoryRepository which would fire an additional query to get referenced images:
public function get(string $documentId) : array
{
$document = $this->createQueryBuilder()
->field('id')->equals($documentId)
->hydrate(false)
->getQuery()
->toArray();
$document['images'] = /* ... */;
return $document;
}

Laravel 5.2 set model attributes based on fields in Input

I have huge user update form. Sometimes update contains huge amount of fields, sometimes just one or two. This is my code:
public function updateUser(Request $request){
$user = User::where('id',$request->id)->firstOrFail();
if($request->first_name){
$user->first_name= $request->first_name;
}
if($request->last_name){
$user->last_name = $request->last_name;
}
if($request->job_name){
$user->job_name= $request->job_name;
}
//etc.. 20 more fields
$user->save();
It is possible to set model attributes dependent on fields in $request? Sometimes $request contains 1 field, sometimes 20. Please notice I want to touch database only once, using save() method at the end.
$user->update($request->all());
Make sure all necessary variables are specified in your $fillable array for User model
If you want to update model attributes without saving use fill method
If $request field name and Model field name are same(as it seems in your current code) try this:
$input = $request->all();
$user = User::firstOrFail('id',$input->id);
$updateNow = $user->update($input);
Another option is:
DB::table('users')
->where('id', $request->id)
->update($request); //or can use Input::all()
Have a look at it as well for more explanation: Query Builder
for User model
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'name', 'email', 'password', 'facebook_id', 'job_name', '20 more fields...'
];
for Controller
public function store(Request $request){
$allRequest = $request->all();
// It is not in table
unset($allRequest['_token']);
User::create($allRequest);
}

Zend duplicated rows on mysql insert

For some reason, when I do an mysql db insert from Zend, my row is dulpicated. I've tried a direct insert via phpmyadmin and it works perfect, so its not a mysql server problem.
This is the code I use:
<?php
class Model_Team extends Zend_Db_Table_Abstract {
protected $_name = 'team';
public function createUser($data) {
$user = $this->createRow();
$user->name = $data['name'];
$user->title = $data['title'];
$id = $user->save();
return $id;
}
}
?>
Thanks in advance.
EDIT:
I've found that this duplication only occurs when i call the form via AJAX (modal box), although the form post is normal, not an ajax request)
I don't know why your code is double pumping the database on save but it should'nt matter as you're using the Row object and save(). (save() inserts or updates)
You may want to restructure your createUser() function so that it can't create a new row if the row already exists.
<?php
class Model_Team extends Zend_Db_Table_Abstract {
protected $_name = 'team';
public function createUser(array $data) {
$user = $this->createRow();
//test if user has id in the array
if (array_key_exists('id', $data)){
$user->id = $data['id'];
}
$user->name = $data['name'];
$user->title = $data['title'];
$user->save();
//no need to create a new variable to return the user row
return $user;
}
}
This method will create and update a user row.
To help you further I'll need to see the controller code most of my double pumps have happened there.
Instead of using createRow() have you tried using insert()?
/**
* Insert array of data as new row into database
* #param array $data associative array of column => value pairs.
* #return int Primary Key of inserted row
*/
public function createUser($data)
{
return $this->insert($data);
}
Also - could we see the ajax code? It may be that the form is being posted as well?

Using Image Content Objects from tt_content in Extbase

I want to write an Extbase Backend module which needs a list of all Objects generated from tt_content with CType = 'image'.
Now I started defining a simple model
class Tx_Myextension_Domain_Model_Content extends Tx_Extbase_DomainObject_AbstractEntity
{
/**
* #var string
*/
protected $header;
/**
* #return the $header
*/
public function getHeader()
{
return $this->header;
}
/**
* #param string $header
*/
public function setHeader($header)
{
$this->header = $header;
}
}
and a Repository
class Tx_Myextension_Domain_Repository_ContentRepository extends Tx_Extbase_Persistence_Repository
{
public function initializeObject()
{
$querySettings = $this->objectManager->create('Tx_Extbase_Persistence_Typo3QuerySettings');
$querySettings->setRespectStoragePage(FALSE);
$this->setDefaultQuerySettings($querySettings);
}
}
As far as I know the initializeObject method is a way to get all content elements, no matter which pid they have.
At last I tried to map my Content Class on tt_content:
plugin.tx_myextension {
persistence {
classes {
Tx_Myextension_Domain_Model_Content {
mapping {
tableName = tt_content
recordType = Tx_Myextension_Domain_Model_Content
columns {
header.mapOnProperty = header
}
}
}
}
}
}
module.tx_myextension {
persistence < plugin.tx_myextension.persistence
}
No I want to use the Repo. e.g. countAll. Unfortunately it always returns 0. Looking for the MySQL query discovers the problem:
SELECT COUNT(*)
FROM tt_content
WHERE (tt_content.CType='Tx_Myextension_Domain_Model_Content')
AND tt_content.deleted=0 AND tt_content.hidden=0
AND (tt_content.starttime<=1313073660)
AND (tt_content.endtime=0 OR tt_content.endtime>1313073660)
AND tt_content.sys_language_uid IN (0,-1)
AND tt_content.pid IN (0)
Typo 3 or Extbase or something different added all these where clauses to the query. I just want to get rid of the CType and pid clauses. As I said, I thought that the method used in the Repo leads to ignoring the pid, which is obviously not the case.
Can somebody help me? All I want is an array of Image Content Elements. Thank you in advance.
Late answer: You'll most likely want to call
query->getQuerySettings()
->setRespectEnableFields(FALSE)
->setRespectSysLanguage(FALSE);
for your query. You can disable it for all queries in your repository's initializeObject method:
$querySettings = $this->objectManager->create('Tx_Extbase_Persistence_Typo3QuerySettings');
$querySettings
->setRespectStoragePage(FALSE)
->setRespectEnableFields(FALSE)
->setRespectSysLanguage(FALSE);
$this->setDefaultQuerySettings($querySettings);
See: TYPO 3 API docs
Try to remove the Node "recordType" from your Persistence Definition.