I am quite new to Typo3 and Extbase so I am really kind of lost right now… We have a category tree like that:
+ root
++ chairs
++ wooden chairs
+ plastic chairs (2)
+ generic chairs (0)
+ broken chairs
++ slightly broken chairs (3)
+ heavily broken chairs (1)
+ chairs that kill (10)
And we have a dataset (custom extension and database table etc.) and each record can be assigned to a category, what is a built in functionality of typo3. We also have a Repository class which implements a findByCategory() method.
We would like to list all records from the custom table, which are member of a given category. So in case of the example above, findByCategory('broken chairs') should yield 14 items and findByCategory('chairs') 16.
Is there any Helper/Utility Class we can use to obtain all categories from a given parent category?
I think you have to extend the CategoryRepository and implement your own method. The method could look like this :
/**
* Finds categories based on their parents, possibly taking categories2skip into account
*
* #param integer $parent
* #param array $categories2skip
*
* #return object
*/
public function findByParent($parent, $categories2skip = array()) {
$query = $this->createQuery();
$constraints = array();
$constraints[] = $query->equals('parent', $parent);
if (count($categories2skip) > 0) {
$constraints[] = $query->logicalNot($query->in('uid', $categories2skip));
}
$query->matching(
$query->logicalAnd($constraints)
);
$result = $query->execute();
return $result;
}
Related
I'm going to write REST API for my project. I'm using symfony 4. I saw several examples, but non of them fit me.
Validation with Form object. It doesn't work for me, because it's API, there are no forms. I don't want to write dummy classes just to support this functionality.
On this page https://symfony.com/doc/current/validation.html they suggest 4 ways: Annotation, yml, xml, php. This solution doesn't fit me because this validation is related to the entity, API - is much mode wider: it has limit, offset, filters and other fields, that doesn't belong to an entity.
So, I think I need to write validator which has an array of constraints for all possible fields. I just don't know what is the best way to present this. Have you ever seen something similar?
P.S. Before writing this post I used stackoverflow search. I didn't find useful answers.
Looking at your example (example.com/api/categories?limit=20&offset=300&filter=something) I guess your action would look something like this:
public function getCategories(?int $limit, ?int $offset, ?string $filter)
{
//...
}
Collection validation
You can define your constraints as an array (and later abstract it away into its own class), and pass it as the second argument to your validator.
$constraint = new Assert\Collection([
'limit' => [
new Assert\Range(['min' => 0, 'max' => 999]),
new Assert\DivisibleBy(0.5)
],
'offset' => new Assert\Range(['min' => 0, 'max' => 999]),
'filter' => new Assert\Regex("/^\w+/")
]);
$validationResult = $this->validator->validate(
['limit' => $limit, 'offset' => $offset, 'filter' => $filter],
$constraint
);
Documentation link.
Validate one by one
Pass the constraint to the validator as second argument, for every parameter you want to validate.
$offsetValidationResult = $this->validator->validate(
$offset,
new Assert\Range(['min' => 0, 'max' => 999])
);
//...
Documentation link.
Object validation
Create a class with the 3 fields in it.
class FilterParameters
{
public function __construct($limit, $offset, $filter)
{
$this->limit = $limit;
$this->offset = $offset;
$this->filter = $filter;
}
// No getters/setters for brevity
/**
* #Assert\DivisibleBy(0.25)
*/
public $limit;
/**
* #Assert\Range(min = 0, max = 999)
*/
public $offset;
/**
* #Assert\Regex("/^\w+/")
*/
public $filter;
}
Instantiate and validate it.
$validationResult = $this->validator->validate(
new FilterParameters($limit, $offset, $filter)
);
Documentation link.
I think to use forms as usual is the very clean and nice.
https://codereviewvideos.com/course/beginners-guide-back-end-json-api-front-end-2018/video/validating-json-data-symfony
I choose this api, because it was the fastest in my tests.
You do not have to buy the course (but you might if you like the code), just follow the "raw symfony 4" articles in this series (you also dont need the behat part)
"Limit", "offset" and "filter" functionality belongs to your repositories. Same way as you pass the id here to the repository
/**
* Class AlbumController
* #package App\Controller
*/
class AlbumController extends AbstractController
{
// ....
/**
* #Route(
* path = "/api/album/{id}",
* name = "get_album",
* methods = {"GET"},
* requirements = {"id"="\d+"}
* )
* #param int $id
*
* #return JsonResponse
*/
public function get($id)
{
return new JsonResponse($this->findAlbumById($id), JsonResponse::HTTP_OK);
}
/**
* #param $id
*
* #return Album|null
* #throws NotFoundHttpException
*/
private function findAlbumById($id)
{
$album = $this->albumRepository->find($id);
if ($album === null) {
throw new NotFoundHttpException();
}
return $album;
}
Scenario:
I have following model:
ContactPerson has a relation to FrontendUser and is the owning side of the relation. Now I have following problem:
I am activating/deactivating the FrontendUsers in a task, based on the ContactPersons which are active. When the FrontendUser is disabled or deleted the result of contactPerson->getFrontendUser() is null, even if both repositories ignoreEnableFields:
/** #var Typo3QuerySettings $querySettings */
$querySettings = $this->objectManager->get(Typo3QuerySettings::class);
$querySettings->setIgnoreEnableFields(true);
$querySettings->setRespectStoragePage(false);
$this->frontendUserRepository->setDefaultQuerySettings($querySettings);
$debugContactPerson = $this->contactPersonRepository->findOneByContactPersonIdAndIncludeDeletedAndHidden('634');
$debugFrontendUser = $this->frontendUserRepository->findOneByUid(7);
\TYPO3\CMS\Extbase\Utility\DebuggerUtility::var_dump(
array(
'$debugContactPerson' => $debugContactPerson,
'$debugFrontendUser' => $debugFrontendUser,
)
);
Result:
P.s.: $this->frontendUserRepository->findByUid(7); also doesn't work because it isn't using the query, but persistenceManager->getObjectByIdentifier(... which is of course ignoring the query-settings.
The problem is, in my real code I can't use findOneByUid(), because I can't get the integer-Value(uid) in the frontend_user field of the contact_person.
Any way to solve this without using a raw query to get the contact_person-row?
My (yes raw query) Solution:
Because I didn't want to write an own QueryFactory and I didn't want to add a redundant field to my contactPerson I solved it now with a raw statement. Maybe it can help someone with the same problem:
class FrontendUserRepository extends \TYPO3\CMS\Extbase\Domain\Repository\FrontendUserRepository
{
/**
* #param \Vendor\ExtKey\Domain\Model\ContactPerson $contactPerson
* #return Object
*/
public function findByContactPersonByRawQuery(ContactPerson $contactPerson){
$query = $this->createQuery();
$query->statement(
"SELECT fe_users.* FROM fe_users" .
" LEFT JOIN tx_extkey_domain_model_contactperson contact_person ON contact_person.frontend_user = fe_users.uid" .
" WHERE contact_person.uid = " . $contactPerson->getUid()
);
return $query->execute()->getFirst();
}
}
Invoking repository directly
There are two aspects for the enable fields of table fe_users:
$querySettings->setIgnoreEnableFields(true);
$querySettings->setEnableFieldsToBeIgnored(['disable']);
Have a look to some overview in the wiki page - it says 6.2, but it's still valid in most parts for 7.6 and 8 as well. However, this only works if the repository is invoked directly, but not if an entity is retrieved as part of another entity - in this case the repository is not used for nested entities.
Modify query settings for nested entities
Nested entities are retrieved implicitly - this happens in DataMapper::getPreparedQuery(DomainObjectInterface $parentObject, $propertyName). To adjust query settings for child entities, the QueryFactoryInterface implementation has to be overloaded.
Register an alternative implementation in ext_localconf.php (replace \Vendor\ExtensionName\Persistence\Generic\QueryFactory with the real class name of your extension):
$extbaseObjectContainer = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(
\TYPO3\CMS\Extbase\Object\Container\Container::class
);
$extbaseObjectContainer->registerImplementation(
\TYPO3\CMS\Extbase\Persistence\Generic\QueryFactoryInterface::class,
\Vendor\ExtensionName\Persistence\Generic\QueryFactory::class
);
With new Typo3 versions (v8+), the registerImplementation method no longer works for QueryFactory. Instead, a XCLASS must be used to overwrite/extend the class:
$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][\TYPO3\CMS\Extbase\Persistence\Generic\QueryFactory::class] = [
'className' => \Vendor\ExtensionName\Persistence\Generic\QueryFactory::class,
];
Then inside the implementation:
<?php
namespace \Vendor\ExtensionName\Persistence\Generic;
use TYPO3\CMS\Extbase\Domain\Model\FrontendUser;
class QueryFactory extends \TYPO3\CMS\Extbase\Persistence\Generic\QueryFactory {
public function create($className) {
$query = parent::create($className);
if (is_a($className, FrontendUser::class, true)) {
// #todo Find a way to configure that more generic
$querySettings = $query->getQuerySettings();
$querySettings->setIgnoreEnableFields(true);
// ... whatever you need to adjust in addition ...
}
return $query;
}
}
My solution of this problem was to disable the "enablecolumns" in TCA definitions and deal this in the repository myself.
Here an example of findAll method:
public function findAll($ignoreEnableFields = false) {
$query = $this->createQuery();
if (!$ignoreEnableFields) {
$currTime = time();
$query->matching(
$query->logicalAnd(
$query->equals("hidden", 0),
$query->logicalOr(
$query->equals("starttime", 0),
$query->lessThanOrEqual("starttime", $currTime)
),
$query->logicalOr(
$query->equals("endtime", 0),
$query->greaterThanOrEqual("endtime", $currTime)
)
)
);
}
return $query->execute();
}
Magento 2: Get Product Stock Quantity and Other Stock Information
How to get the product stock quantity and information in magento 2
If we look at the StockItemRepository class the get method wants parameter $stockItemId, not $productId. Reference:
https://github.com/magento/magento2/blob/develop/app/code/Magento/CatalogInventory/Model/Stock/StockItemRepository.php#L202
I've seen many sites where stock item id IS NOT the same as product id and we should not assume it's the same ID.
To get this working you could use \Magento\CatalogInventory\Model\Stock\Item class instead and load the model by product_id field instead. I am also aware of the website_id and stock_id fields, but as far as I know it's not used (yet) and also existed in M1.
It should look something like this (code not tested):
<?php
namespace Vendor\Module\Model;
use \Magento\CatalogInventory\Model\Stock\Item;
class Mymodel
{
/**
* #var Item
*/
protected $stockItem;
/**
* Mymodel constructor.
*
* #param Item $stockItem
*/
public function __construct(Item $stockItem)
{
$this->stockItem = $stockItem;
}
/**
* Description
*
* #param $productModel
*/
public function getStockQtyByProductId($productModel)
{
try {
$stockItem = $this->stockItem->load($productModel->getId(), 'product_id');
return $stockItem->getQty();
} catch (\Exception $e) {
echo 'Something went wrong and was not handled: ' . $e->getMessage();
exit;
}
}
}
if you have product object then just use following:
echo $_product->getExtensionAttributes()->getStockItem()->getQty();
conplete object can be find as follow:
var_dump($_product->getExtensionAttributes()->getStockItem()->getData());
Actually this operation should be performed using \Magento\CatalogInventory\Api\StockRegistryInterface and here we can obtain \Magento\CatalogInventory\Api\Data\StockItemInterface, by product id or sku and we can use bunch of usefull methods to get stock information - linked product. For general stock information I recommend explore other service contracts declared in Magento\CatalogInventory\Api
Example of usage:
<?php
namespace Test\Test\Model;
class Test
{
protected $_stockRegistry;
public function __construct(\Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry)
{
$this->_stockRegistry = $stockRegistry;
}
public function getStockItem($productId)
{
return $this->_stockRegistry->getStockItem($productId);
}
}
this code help you to get product quantity
<?php
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$StockState = $objectManager->get('\Magento\CatalogInventory\Api\StockStateInterface');
echo $StockState->getStockQty($productId);
?>
If you have the product object and do not want to use the other classes, You can try in the following way.
// For phtml file
$prodObj = $_product->load($_product->getId()); // $_product object in list.phtml
$stockItem = $prodObj->getExtensionAttributes()->getStockItem();
$stockQty = $stockItem->getQty(); // $stockItemData = $stockItem->getData();
// For php class file
$stockItem = $prodObj->getExtensionAttributes()->getStockItem();
$stockQty = $stockItem->getQty(); // $stockItemData = $stockItem->getData();
Credits:
https://github.com/magento/magento2/issues/7057#issuecomment-256052729
Actually \Magento\CatalogInventory\Api\Data\StockStatusInterface
should answer to all your questions.
Long story short:
Magento has StockItem entity which represents amount (Qty) of specific product (productId) on a concrete stock (stockId).
StockItemInterface should be used when you would like to "write" data into the data storage (like update amount of products to sync up Magento with your ERP system or to make deduction of stock during the checkout process).
StockStatusInterface is opposite to it. It should be used to "read" data for representation (on front-end). Consider StockStatus as an index which contains aggregated stock data for each specific product.
So, if you would like to get product stock status (in stock, out of stock) by product_id.
You need using StockStatusRepositoryInterface::getList(StockStatusCriteriaInterface $searchCriteria);
get StockStatus entity for specified product
/** #var \Magento\CatalogInventory\Api\StockStatusCriteriaInterfaceFactory $stockStatusCriteriaFactory **/
$criteria = $stockStatusCriteriaFactory->create();
$criteria->setProductsFilter($productId);
/** #var \Magento\CatalogInventory\Api\Data\StockStatusRepositoryInterface $stockStatusRepository **/
$result = $stockStatusRepository->getList($criteria);
$stockStatus = current($result->getItems());
$stockStatus->getProductId(); // product id
$stockStatus->getQty(); // quantity of specified product
$stockStatus->getStockStatus(); // Could be
// Magento\CatalogInventory\Model\Stock\Status::STATUS_OUT_OF_STOCK = 0;
// or
// Magento\CatalogInventory\Model\Stock\Status::STATUS_IN_STOCK = 1;
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.
I have 3 models that I have setup thus far in a simple application I am working on:
So far I have these models:
UserAccountEntity - Top level Table (Has a One-Many Relationship to UserAccountEntityStrings)
UserAccountEntityStrings - Child Table (Has a Many-One relation ship to UserAccountEntity and EavAttributes
EavAttributes - Lookup Table
When I query data from my top level table, I get the schema,association information for the child table. But I do not get any of the persisted data from the child table.
What I expected the results to be were, the data from the top level model and the data from the associated child model. Any help with this is greatly appreciated.
A note that may be helpful, I am using Zend 1.11.10 and Doctrine 2
This is what my query looks like:
$users = $em->createQuery('select u from Fiobox\Entity\UserModule\UserAccountEntity u')->execute();
Zend_Debug::dump($users[0]);
This is the association in my top level model:
/**
*
* #param \Doctrine\Common\Collections\Collection $property
* #OneToMany(targetEntity="UserAccountEntityStrings",mappedBy="UserAccountEntity", cascade={"persist","remove"})
*/
private $strings;
These are the associations in my child model:
/**
*
* #var UserAccountEntity
* #ManyToOne(targetEntity="UserAccountEntity")
* #JoinColumns({
* #JoinColumn(name="entity_id", referencedColumnName="entity_id")
* })
*/
private $user;
/**
* #var EavAttribute
* #ManyToOne(targetEntity="Fiobox\Entity\EavModule\EavAttributes")
* #JoinColumn(name="attribute_id", referencedColumnName="attribute_id")
*/
private $attributes;
Have you actually tried anything?
Doctrine will lazy load stuff for you. Your var_dump probably shows persistent collections of proxy objects for your child objects. But if you access them, they'll be loaded automatically:
<?php
$users = $em->createQuery('select u from Fiobox\Entity\UserModule\UserAccountEntity u')->fetchAll();
foreach($users as $u){
foreach($u->strings as $s){
var_dump($s);
}
}
If you know that you're going to need all that child data, you might as well force a fetch-join in your DQL:
<?php
$users = $em->createQuery('select u, s from Fiobox\Entity\UserModule\UserAccountEntity u JOIN u.strings s')->fetchAll();