'CakePHP Entity vs EntityInterface
Because PHPStan is driving me nuts about this, I have to ask and hope someone has an answer: when querying the database, the result set is a list of EntityInterface objects. Then when I attempt to work with the results, PHPStan gets suspicious of the objects and whether they contain the right fields or not. If I pass the following to a function:
$this->Carts->get($cart->id);
The error PHPStan give is phpstan: Cannot access property $id on array|Cake\Datasource\EntityInterface|null. This gets especially aggravating when I'm working with users which are also Authentication objects.
In the end, I have a ton of clauses in my code that look like this to bypass PHPStan's error messages:
if (is_object($cart) && is_a($cart, 'Cake\Datasource\EntityInterface')) {
$this->CartManager->pruneCarts($user->id, (int)$cart->id);
}
But if I'm verifying that a given object is an EntityInterface, then PHPStan will tell me that EntityInterface does not have a property called id.
It seems like there must be some larger check I should be doing where I verify that the object IS and object and IS an EntityInterface AND IS the specific type of Entity that I know I've selected. But do I really need to wrap every such query in three layers of checks?
I have both of the following plugins installed, which should be mitigating the problem, but are not:
- "dereuromark/cakephp-ide-helper": "^1.13",
- "cakedc/cakephp-phpstan": "^2.0",
Currently, the get() function is documented in the following manner, in every table:
@method \Visualize\Model\Entity\Cart get($primaryKey, $options = [])
Here is a more fulsome example of the problem I'm having. All the way down, when I check the properties of $cart in this example function, it is declared as $cart array|Cake\Datasource\EntityInterface|null and ultimately, my function is telling me that I'm returning the wrong thing. I don't know how to remove the uncertainty of array|Cake\Datasource\EntityInterface|null and collapse it down to a single thing?
/**
* Returns the most recent active cart, else creates a new empty one.
*
* @param \Visualize\Model\Entity\User $user The User entity.
* @return \Cake\Datasource\EntityInterface
*/
public function getUserCart(User $user)
{
$query = $this->Carts->find('userCart', ['user_id' => $user->id]);
if (!$query->isEmpty()) {
$cart = $query->first();
if (!empty($cart->id)) {
$this->updateSessionCart((int)$cart->id);
} else {
Log::error('User shopping cart does not exist.');
}
} else {
$cart = $this->Carts->newEmptyEntity();
$cart->set('user_id', $user->id);
$cart->set('cart_total', 0);
$cart->set('cart_status', Configure::read('Orders.NewCart'));
$this->Carts->save($cart);
$this->updateSessionCart((int)$cart->id);
}
return $this->Carts->get($cart->id);
}
EDIT
Here is the entire CartManagerComponent for the sake of completeness. @mark will note that the $Carts variable IS set to the CartsTable object.
<?php
declare(strict_types=1);
namespace Visualize\Controller\Component;
use Cake\Controller\Component;
use Cake\Core\Configure;
use Cake\Log\Log;
use Cake\ORM\TableRegistry;
use Visualize\Model\Entity\User;
/**
* CartManager component
*
* @method \Visualize\Controller\AppController getController()
* @property \Visualize\Controller\AppController $Controller
* @property \Visualize\Model\Table\CartsTable $Carts
* @property \Visualize\Model\Table\CartLinesTable $CartLines
*/
class CartManagerComponent extends Component
{
/**
* Default configuration.
*
* @var array<string, mixed>
*/
protected $_defaultConfig = [];
/**
* @var \Visualize\Controller\AppController
*/
protected $Controller;
/**
* @var \Cake\ORM\Table
*/
protected $Carts;
/**
* @var \Cake\ORM\Table
*/
protected $CartLines;
/**
* @var \Cake\Http\Session
*/
protected $Session;
/**
* @param array $config The current configuration array
* @return void
*/
public function initialize(array $config): void
{
parent::initialize($config);
$this->Controller = $this->getController();
$this->Carts = TableRegistry::getTableLocator()->get('Carts');
$this->CartLines = TableRegistry::getTableLocator()->get('CartLines');
$this->Session = $this->Controller->getRequest()->getSession();
}
/**
* Returns the most recent active cart, else creates a new empty one.
*
* @param \Visualize\Model\Entity\User $user The User entity.
* @return \Cake\Datasource\EntityInterface
*/
public function getUserCart(User $user)
{
$query = $this->Carts->find('userCart', ['user_id' => $user->id]);
if (!$query->isEmpty()) {
$cart = $query->first();
if (!empty($cart->id)) {
$this->updateSessionCart((int)$cart->id);
} else {
Log::error('User shopping cart does not exist.');
}
} else {
$cart = $this->Carts->newEmptyEntity();
$cart->set('user_id', $user->id);
$cart->set('cart_total', 0);
$cart->set('cart_status', Configure::read('Orders.NewCart'));
$this->Carts->save($cart);
$this->updateSessionCart((int)$cart->id);
}
return $this->Carts->get(1);
}
/**
* Abandons carts
*
* @param int $user_id The associated user ID
* @param int $cart_id The current cart ID
* @return void
*/
public function pruneCarts(int $user_id, int $cart_id): void
{
if (!empty($this->Controller->Carts) && is_a($this->Controller->Carts, '\Visualize\Model\Table\CartsTable')) {
// Find all the carts we didn't just create:
$userCarts = $this->Controller->Carts->find('all', ['fields' => ['id', 'user_id', 'cart_status']])
->where([
'id !=' => $cart_id,
'user_id' => $user_id,
'cart_status' => 'active',
]);
if (!$userCarts->isEmpty()) {
$count = 0;
foreach ($userCarts as $cart) {
if ($count < 5) {
$record = $this->Controller->Carts->newEmptyEntity();
$record = $this->Controller->Carts->patchEntity($record, $cart->toArray());
$record->set('id', $cart->id);
$record->set('cart_status', Configure::read('Orders.AbandonedCart'));
if (!$this->Controller->Carts->save($record)) {
Log::error('Error abandoning cart');
}
} else {
$this->Controller->Carts->delete($cart);
}
$count++;
}
}
}
}
/**
* Update the current session cart line count.
*
* @param int $cart_id The current cart id.
* @return void
*/
public function updateSessionCart(int $cart_id): void
{
$count = $this->CartLines->find('all')
->where(['cart_id' => $cart_id])
->count();
$this->Session->write('Cart.id', $cart_id);
$this->Session->write('Cart.count', $count);
}
/**
* Remove any carts from the user's session.
*
* @return void
*/
public function removeSessionCart(): void
{
$this->Session->delete('Cart');
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|
