I have an Entity Category, which is linked to itself in order to form a tree (a category can have a category as a parent and a category can have a bunch of categories as children). These are marked as private inside the Entity and not exposed to the serializer.
When I do $category->getChildren()->toArray(), I get an array of the children, but when I do $this->getDoctrine()->getRepsitory('PmbLicensing:Category')->findByParent($category)->toArray(), I get an error that toArray() is not defined. I need to use the latter because the top level categories have their parent set to null, so I cannot use the former method. How do I convert the collection of categories obtained in the latter method to an array?
Also, when trying to trouble shoot, I often would like to print out variables, but when I do something like print_r($categories);, print_r((array)$categories); or var_dump($categories); the call just runs for about two minutes and then returns null. I assume it is because of the relational mapping that goes into an infinate loop, but how do I stop this from happening?
Edit: I want to convert the object (or collection of objects) to an array, because I want to build a recursive function where the children categories of the supplied category can be retrieved up to n-depth. If the supplied category can be null, in order to retrieve from the main level of categories (with parent set to null). Here is my function:
private function getRecursiveChildren(Category $category = null, $depth, $iteration)
{
$children = $this->getDoctrine()->getRepository('PmbLicensingBundle:Category')->findByParent($category);
// \Doctrine\Common\Util\Debug::dump($children); die();
if ($depth > $iteration)
foreach ($children as $child) {
$child['children'] = $this->getRecursiveChildren($child, $depth, $iteration+1);
}
return $children;
}
On the line that has $child['children'], is says that I cannot use an object as an array.
If you need the results as an array you can return them from the database as arrays.
In your CategoryRepository class:
public function findArrayByParent($categoryId)
{
// it's a good adivce from #i.am.michiel to pass only the `id` here.
// You don't need the whole category object.
$query = $this->getEntityManager()->createQuery('...')
->setParameters(array('categoryId' => $categroyId));
return $query->getArrayResult();
}
They are never converted to objects after being retrieved from the DB so you also save time and memory.
Actually your
$children = $this->getDoctrine()->getRepository('PmbLicensingBundle:Category')
->findByParent($category);`
already returns an array so you don't have to (and can't) use ->toArray().
When you loop through your categories with
foreach ($children as $child)
$child["..."] = ...
You are treating an obect $child like an array with ["..."]. That's what your error message is about.
If you can, you should probably use doctrine and let it fill the related child and parent categories. See the Doctrine Documentation on this. Then you have automatically all your Children and can access them like $category->getChildren() (This one will return an ArrayCollection). This will save you a lot of work.
Your call simply returns no categories. Btw, I think you should pass the id of the category, not the entity.
$this->getDoctrine()
->getRepository('PmbLicensing:Category')
->findByParent($category->getId());
And why use the toArray() function? ArrayCollection already are arrays with a few additionnal methods? You should be able to use an ArrayCollection whenever you used an array.
Related
How does one get the parent category ID of a category ID in Magento 2?
In Magento 1, I did it with the following:
$product_id = 101; //for example
$product = Mage::getModel('catalog/product')->load($product_id); //get product object by product ID
$category_ids = $product->getCategoryIds(); //array of all categories that the product is in
foreach ($category_ids as $cat_ids) {
$parent_id = Mage::getModel('catalog/category')->load($cat_id)->getParentId(); //
echo $parent_id; //outputs an int ID of parent category
}
In Magento 2, I've been attempting the same with the following:
$product_id = 101; //again, for example
$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$productRepository = $objectManager->create('\Magento\Catalog\Model\ProductRepository');
$product = $productRepository->getById($product_id); //get product object by product ID
$category_ids = $product->getCategoryIds(); //array of all categories that the product is in
foreach ($category_ids as $cat_ids) {
echo $cat_ids;
}
Up to here, my code is working perfectly and the $category_ids is an array of all the categories that the product is in. However I cannot figure out how to get the parent category IDs of each child category ID in the $category_ids array.
NOTICE* I'm aware that I'm not officially supposed to directly used the ObjectManager, so please save this from your answer. I am seeking to specifically use the ObjectManager in this manner to iterate over $category_ids and load the parent category IDs for each child category ID.
Like so often, there are multiple ways to achieve this.
The CategoryFactory route
To load a category directly, you load it via the Factory Singleton responsible for the \Magento\Catalog\Model\Category class. This is the \Magento\Catalog\Model\CategoryFactory class. From each instance of Category, you can simple call the method getParentId() to get the parent ID.
foreach ($categoryIds as $categoryId) {
try {
$category = $this->_categoryFactory->create()->load($categoryId);
} catch (\Exception $e) {
/* Handle this appropriately... */
}
echo 'Parent Category ID: ', $category->getParentId(), PHP_EOL;
}
In this example, $categoryIds is the array of Category IDs you extracted from your \Magento\Catalog\Model\Product instance.
The CategoryRepository route
Or preferably you can use a Singleton instance of the \Magento\Catalog\Model\CategoryRepository class as a wrapper around the Factory. It will handle all the loading with some added error handling and it will also store a reference to the loaded category for later reuse. So if you are doing this multiple times during one execution, or suspect that you will load the same category later on, using the Repository will optimize your performance.
foreach ($categoryIds as $categoryId) {
try {
$category = $this->_categoryRepository->get($categoryId);
} catch (\Exception $e) {
/* Handle this appropriately... */
}
echo 'Parent Category ID: ', $category->getParentId(), PHP_EOL;
}
The Collection route
This should be a much faster route, as you (1) load all categories once from database instead of using several multiple sql calls in the backend and (2) you have some control over what is populated in the Category, and what is left out. Please be aware, that pretty much only what you put in addAttributeToSelect() will be populated in the Collection. But if you're only after the parent_id this should not be an issue.
First, make sure you are familiar with collections, then acquire a CollectionFactory Singleton for Magento\Catalog\Model\ResourceModel\Category\CollectionFactory and then populate it like so:
/** #var \Magento\Catalog\Model\ResourceModel\Category */
$collection = $this->_categoryCollectionFactory->create();
# Specifically select the parent_id attribute
$collection->addAttributeToSelect('parent_id');
# Only select categories with certain entity_ids (category ids)
$collection->addFieldToFilter('entity_id', ['in' => $categoryIds])
# Iterate over results and print them out!
foreach ($collection as $category) {
echo 'Parent Category ID: ', $category->getParentId(), PHP_EOL;
}
With great powers comes great risk, however. This above code will have no error correction whatsoever. If there is a logical database error, such as a product which points to a missing category, this category will just be omitted from the collection and it will be up to you as a programmer to spot that and deal with it. Also, you will have to decide for yourself on how you are handling store view and active/inactive categories via filters to the collection.
The Direct Database route
Ok, I would not recommend this route unless you know exactly what you are doing, and are in desperate need for performance.
This will be crazy-fast, but there are all sorts of problems, like relying on the underlying data storage and data structure, not to mention that you are open to (very unlikely, to be fair) future updates to the underlying database structure, either directly via Magento upgrades or via (nasty) 3rd party modules. Not the mention the dangers of SQL injections or XSS attacks. (Though, you should always keep this in mind, with all 4 methods.)
As you are using the ObjectManager directly, I assume you won't mind these drawbacks, however, so I though I'd give you this option as well.
The basic pseudo-sql is:
select parent_id from <name of catalog_category_entity table> where entity_id in (<sanitized, comma-separated list of category ids);
First, acquire an instance of the \Magento\Framework\App\ResourceConnection class. You will use this to get the necessary table name for catalog_category_entity, as well as getting the database connection. Then you should sanitize your data and finally, the bind and execute the query and fetch your data.
/** #var \Magento\Framework\App\Connection */
$connection = $this->_resourceConnection->getConnection();
# Get prefixed table name of catalog_category_entity
$categoryEntityTableName = $this->_resourceConnection->getTableName('catalog_category_entity');
# Sanitize the $categoryIds array using a bit of overkill
array_walk_recursive($categoryIds, function(&$value, $key){
$value = filter_var($value, FILTER_SANITIZE_NUMBER_INT);
});
# Prepare a sql statement which fetches entity_id and parent_id
$preparedStatement = $this->connection->prepare('select entity_id, parent_id from ' . $categoryEntityTableName . ' where entity_id in (' . implode(',', array_fill(0, sizeof($categoryIds), '?')) . ')');
# Bind sanitized $categoryIds array to statement and execute said statement in one single step
$preparedStatement->execute($categoryIds);
# fetch result as a key-value pair array of entity_id=>parent_id
$parentIds = $preparedStatement->fetchAll(\PDO::FETCH_KEY_PAIR);
# Iterate over results and print them out!
foreach ($parentIds as $categoryId => $parentId) {
echo 'Parent Category ID: ', (int)$parentId, PHP_EOL;
}
Footnote
I assume you are well aware of the pros and cons of using the ObjectManager directly, so I'll spare you the lecture ;-). However, for future reference I'll also have to state to future readers stumbling upon this answer that if they are unaware on how to acquire instances of the CategoryFactory, CategoryRepository, CollectionFactory or ResourceConnection classes, I highly recommend them to do so via the intended Dependency Injection mechanism.
I know that association property in entity is implements \Doctrine\Common\Collections\Collection. I know that in constructor such properties should be initialized:
$this->collection = new \Doctrine\Common\Collections\ArrayCollection()
I know that I can modify collections using ArrayCollection#add() and ArrayCollection#remove(). However I have a different case.
Suppose I have a new simple array of associative entities. Using existing methods I need to check every element in array: if entity collection has it. If no - add array element to entity collection. In addition to this, I need to check every element in entity collection. If any collection element is absent in new array, then I need to remove it from collection. So much work to do trivial thing.
What I want? To have the setProducts method implemented:
class Entity {
private $products;
// ... constructor
public function setProducts(array $products)
{
// synchronize $products with $this->products
}
}
I tried: $this->products = new ArrayCollection($products). However this makes doctrine remove all products and add those ones from $products parameter. I want similar result but without database queries.
Is there any built in solution in Doctrine for such case?
Edit:
I would like to have a method in ArrayCollection like fromArray which would merge elements in collections removing unneeded. This would just duplicate using add/remove calls for each element in collection argumen manually.
Doctrine collections do not have a "merge"-feature that will add/remove entities from an array or Collection in another Collection.
If you want to "simplify" the manual merge process you describe using add/remove, you could use array_merge assuming both arrays are not numeric, but instead have some kind of unique key, e.g. the entity's spl_object_hash:
public function setProducts(array $products)
{
$this->products = new ArrayCollection(
array_merge(
array_combine(
array_map('spl_object_hash', $this->products->toArray()),
$this->products->toArray()
),
array_combine(
array_map('spl_object_hash', $products),
$products->toArray()
)
)
);
}
You might want to use the product id instead of spl_object_hash as 2 products with the same id, but created as separate entities - e.g. one through findBy() in Doctrine and one manually created with new Product() - will be recognized as 2 distinct products and might cause another insert-attempt.
Since you replace the original PersistentCollection holding your previously fetched products with a new ArrayCollection this might still result in unneeded queries or yield unexpected results when flushing the EntityManager, though. Not to mention, that this approach might be harder to read than explicitly calling addElement/removeElement on the original Collection instead.
I would approach it by creating my own collection class that extends Doctrine array collection class:
use Doctrine\Common\Collections\ArrayCollection;
class ProductCollection extends ArrayCollection
{
}
In the entity itself you would initialise it in the __constructor:
public function __construct()
{
$this->products = new ProductCollection();
}
Here, Doctrine will you use your collection class for product results. After this you could add your own function to deal with your special merge, perhaps something:
public function mergeProducts(ProductCollection $products): ProductCollection
{
$result = new ProductCollection();
foreach($products as $product) {
$add = true;
foreach($this->getIterator() as $p) {
if($product->getId() === $p->getId()) {
$result->add($product);
$add = false;
}
}
if($add) {
$result->add($product);
}
}
return $result;
}
It will return a brand new product collection, that you can replace your other collection in the entity. However, if the entity is attached and under doctrine control, this will render SQL at the other end, if you want to play with the entity without risking database updates you need to detach the entity:
$entityManager->detach($productEntity);
Hopes this helps
I have a simple CRUD item called Filters. In here each filter is assigned to a category with a foreign key. What I am trying to do is loop through each foregin key to get the category name rather than id to display to the user.
I get all filters first and performed a die/dump to check all results were there and they are.
When trying to assign the category name to the correct array item I get this error:
"Indirect modification of overloaded element of App\Filter has no effect"
So to check what's happening I have die/dumped inside the foreach loop and the exact same data has now disappeared. Even if I just put a foreach loop in with no modiifcaiton of the original array, when I pass this back to the view it has been unset.
AM I being very naive and not realising something this foreach loop does that destroys this data???
I have copied my code below and commented where the dd works and doesn't;
public function show()
{
$filter = [];
$filter['filters'] = Filter::all();
//dd($filter['filters']); --this works fine here
foreach($filter['filters'] AS $key => $filter){
//dd($filter['filters']); --this returns null here
$category = Category::where('id', $filter->category)->first();
$filter['filters'][$key]->category = $category->category;
}
return view('admin.crud.filters.index')->with('filter', $filter);
}
Don't re-initialize a variable or different data-type with same name
Try to change the $filter to something else in foreach loop. Because you already have an array with same name.
Do something like:
foreach($filter['filters'] AS $key => $f){...}
The question boils down to finding the proper way how to getPrimaryKey when iterating over a yielded result. When using select method, the result is an object of ArrayCollection which doesn't provide the getPrimaryKey method. A simple snippet
$q = UserQuery::create();
$q->select('a', 'b'); //yields an ArrayCollection object, doesn't have getPrimaryKey method when iterated
$q->find();
However,
$q = UserQuery::create();
$q->find(); //yields an ObjectCollection object, has getPrimaryKey method when iterated
Update
I have tried to use the setFormater to force using the ObjectCollection. Ultimately, it resulted in exception being thrown.
$q = UserQuery::create()
->setFormater(ModelCriteria::FORMAT_OBJECT)
->select('a', 'b')
->find(); //Error populating object
Update 2
Providing an exact use case, since it may be unclear at first what I am looking for. I have a table with >100 columns. I am providing the functionality using behaviour to not disable (not select) some of them. Thus, I am unseting some of the columns, and basing the $q->select on the remaining ones.
if (!empty($tableQuery->tableColumnsDisable)) {
$columns = $tableQuery->getTableMap()->getColumns();
foreach ($columns as $index => $column) {
if (!empty($tableQuery->tableColumnsDisable[$column->getName()])) {
unset($columns[$index]);
continue;
}
$columns[$index] = $column->getName();
}
//TODO - returns array collection, object collection needed
$tableQuery->select($columns);
}
When using select(), Propel will skip object hydration and will just return an ArrayCollection containing an array for each result row.
To retrieve the id of each result row, you need to add the column name to the select(). You can then just retrieve the value from the row arrays by using the column name:
$users = UserQuery::create()
->select(['id', 'a', 'b'])
->orderBy('c')
->find();
foreach ($users as $user) {
$id = $user['id'];
}
The select functionality is described in the documentation and in the docblock of Propel\Runtime\ActiveQuery\ModelCriteria#select() (source).
When you are using Propel 1, the functionality is the same. Read more about it in the Propel 1 documentation or the docblock of ModelCriteria#select() (source).
Trying to get child of a specific category which is active. Please help. I am having trouble doing it. I'm currently able to show them all but not specifically. Would appreciate any help.
$category = Mage::getModel('catalog/category')->load(2);
$category->getChildCategories();
$tree = $category->getTreeModel();
$tree->load();
$ids = $tree->getCollection()->getAllIds();
here is code to load active category
/* Load category by id*/
$cat = Mage::getModel('catalog/category')->load($id);
/*Returns comma separated ids*/
$subcats = $cat->getChildren();
//Print out categories string
#print_r($subcats);
foreach(explode(',',$subcats) as $subCatid)
{
$_category = Mage::getModel('catalog/category')->load($subCatid);
if($_category->getIsActive())
{
$caturl = $_category->getURL();
$catname = $_category->getName();
if($_category->getImageUrl())
{
$catimg = $_category->getImageUrl();
}
echo '<h2><img src="'.$catimg.'" alt="" />'.$catname.'</h2>';
}
}
?>
hope this is sure help you.
As mentioned by mhaupt, it is faster to load a collection rather than each category in a loop. But, as far as I am concerned, there is no need to manually load the child categories. Basically this is what $category->getChildrenCategories() already does.
There is also a filter to get active categories only. Just call addIsActiveFilter() on the collection.
a.) Load active child categories via getChildren()
// 1. Get a list of all child category ids (e.g "12,23,11,42")
$subcategoryIds = $category->getChildren();
// 2. Create collection
$categoryCollection = Mage::getModel('catalog/category')->getCollection();
// 3. Add all attributes to select, otherwise you can not
// access things like $cat->getName() etc.
$categoryCollection->addAttributeToSelect('*');
// 4. Filter by ids
$categoryCollection->addIdFilter($subcategoryIds);
// 5. Add filter to collection to get active categories only
$categoryCollection->addIsActiveFilter();
b.) Load active child categories with getChildrenCategories()
// 1. Load collection
$categoryCollection= $category->getChildrenCategories();
// 2. Add filter to collection to get active categories only
$categoryCollection->addIsActiveFilter();
The collection will be loaded form the database as soon as it is accessed. If the collection is not loaded and $subcategories->count() is called only a "SELECT count(*)" will be fired against the database (in contrast to count($subcategories) which will force the collection to load itself).
Iterating the collection
foreach($categoryCollection as $category) {
echo $category->getName();
}
If you add more filters to the collection after accessing it, the collection will not load itself again automatically. To apply changes to the collection, just call $categoryCollection->load() to reload the collection from the database.
Those who are saying to use getAllChildren() instead of getChildren() are simply wrong.
Both methods return the exact same thing, with one difference, getAllChildren(true) will return an array instead of a comma delimited string. getAllChildren($bool asArray) defaults to false. My point being that either way you're going to have to use
Mage::getModel('catalog/category')->load($catId);
inside of a loop unless you use the function below.
private function fetchCatsById($onlyThese)
{
$cats = Mage::getModel('catalog/category')
->getCollection(true)
->addAttributeToSelect('*')
->addIdFilter($onlyThese)
->addAttributeToFilter('level','2')
->addIsActiveFilter();
return $cats;
}
$cats = $this->fetchCatsById($onlyThese);
The one answer liyakat wrote, should not be used in professional shops, because it raises a performance issue, because of the multiple n time loads of the category object, rather use the collection of categories for that, get all children
$cat->getAllChildren()
, then limit the category collection by the needed category ids like
$coll->addIdFilter($idFilter);
then you won't have to load n times against the database.
Please do keep in mind that loads within loops are one of the most often used bad code examples in any Magento projects and to avoid them!
Hello you will see below code
$category_model = Mage::getModel('catalog/category');
$_category = $category_model->load(13);
$all_child_categories = $category_model->getResource()->getAllChildren($_category);
print_r($all_child_categories);
If you want any number of subcategories of parent category than Click here http://magentoo.blogspot.com/2014/01/get-all-subcategories-of-parent-category-magento.html