Framework: Laravel 5.1
Background: I have a Product and a ProductVariation model, which both share a ImplementsProductAttributes trait. If a Product is variable, than the product should contain at least a single purchasable ProductVariation to be purchasable, rest of the logic would be obvious within the code sample from the trait. Functionality works as is.
public function getPurchasableAttribute()
{
if ($this instanceof \App\Product)
{
if ($this->type == 'variable')
{
foreach ($this->variations as $variation)
{
if ($variation->purchasable)
return true;
}
return false;
}
else return $this->isItemPurchasable();
}
else
{
return $this->isItemPurchasable();
}
}
public function isItemPurchasable()
{
if($this->regular_price > 0 AND $this->published)
{
if ($this->manage_stock)
{
return $this->stock_quantity > 0;
}
else return $this->in_stock;
}
else return false;
}
Pros:
I make use of laravels attributes,
Readable/maintainable code,
Cons:
Redundant eager loading on the variations (which could be a problem
if this was supposed to be used on an API)
Redundant SQL queries (I could implement a single query to get a
count in the end, and decide based on that if the product itself is
purchasable, but instead I am loading all variations -- at least
until i get one with purchasable=true --)
Question: Should I keep this implementation or should I improve by getting rid of pulling relationships and making a simple SQL query to get the purchasable variations?
Please elaborate on your answer as much as you can.
Edit: Also please let me know if you think any other pros/cons exist.
First of all, as per the concern of coding standard, you can get rid of this nested if-else structure in getPurchasableAttribute() method like this
public function getPurchasableAttribute()
{
if (!( $this instanceof \App\Product) || $this->type != 'variable')
{
return $this->isItemPurchasable();
}
foreach ($this->variations as $variation)
{
if ($variation->purchasable)
return true;
}
return false;
}
Can you please tell something more about $variation->purchasable
Is purchasable an eloquent relationship defined in $variation or an attribute of $variation.
So that we can talk something more on efficiency.
Related
I have this route
Route::get('/post/{post:uuid}', [\App\Http\Controllers\PostController::class, 'showPost']);
And it works, if the user inputs an inexisting uuid, the app responses a 404 error, but now I want to add one more condition by using enums on route.
I have an enum called PostStateEnum.php
<?php
namespace Modules\Muse\Enum;
use App\Http\Traits\EnumTrait;
enum PostStateEnum: string
{
use EnumTrait;
case DRAFT = 'draft';
case WAITING_APPROVAL = 'waiting_approval';
case APPROVED = 'approved';
case REJECTED = 'rejected';
case PUBLISHED = 'published';
case UNPUBLISHED = 'unpublished';
}
I want to add a condition in the route: if the $post->state is PostStateEnum::PUBLISHED I want to go to the 'showPost' in my PostController
Currently, I'm handle that logic on my controller
public function showPost(Post $post)
{
if ($post->state == PostStateEnum::PUBLISHED)
{
dump($post);
} else {
return abort(404);
}
}
According to the laravel 9 docs I understand is that I need to create another enum with only one state to be able to validate that from the route, is that correct?
Is possible? Or my way is better?
I think you are confusing what enums in the route can bring. It is not about what is already saved, but more to use it as a filter / input. Imagine you want to have a route, that show posts based on status.
Route::get('posts/{PostStateEnum}');
In your controller you would be able to filter based on that.
public function index(PostStateEnum $enum) {
if ($enum ==PostStateEnum::PUBLISHED) {
// query filter published
} else if ($enum ==PostStateEnum::UNPUBLISHED) {
// query filter unpublished
}
}
Your enum is not from the input, but from the model, therefor what you are doing is actually the correct aproach. If not done, remember to cast your enum.
class Post extends Model {
protected $casts = [
'status' => PostStateEnum::class,
];
}
As a more general code improvement tip, doing if else, like you did in your example is non optimal for readability, you can in these cases, reverse the if logic and do an early return approach.
public function showPost(Post $post)
{
if ($post->state !== PostStateEnum::PUBLISHED)
{
return abort(404);
}
return $post;
}
I am not sure whether this method inside my class is violating Single responsibility principle,
public function save(Note $note)
{
if (!_id($note->getid())) {
$note->setid(idGenerate('note'));
$q = $this->db->insert($this->table)
->field('id', $note->getid(), 'id');
} else {
$q = $this->db->update($this->table)
->where('AND', 'id', '=', $note->getid(), 'id');
}
$q->field('title', $note->getTitle())
->field('content', $note->getContent());
$this->db->execute($q);
return $note;
}
Basically it does two jobs in a method - insert or update.
Should I separate it into two methods instead to comply with Single responsibility principle?
But SRP is meant for classes only, isn't it? Does it apply to the methods inside a class?
SRP -
a class should have only a single responsibility (i.e. only one
potential change in the software's specification should be able to
affect the specification of the class)
EDIT:
Another method for listing notes (including many different type of listings), searching notes, etc...
public function getBy(array $params = array())
{
$q = $this->db->select($this->table . ' n')
->field('title')
->field('content')
->field('creator', 'creator', 'id')
->field('created_on')
->field('updated_on');
if (isset($params['id'])) {
if (!is_array($params['id'])) {
$params['id'] = array($params['id']);
}
$q->where('AND', 'id', 'IN', $params['id'], 'id');
}
if (isset($params['user_id'])) {
if (!is_array($params['user_id'])) {
$params['user_id'] = array($params['user_id']);
}
# Handling of type of list: created / received
if (isset($params['type']) && $params['type'] == 'received') {
$q
->join(
'inner',
$this->table_share_link . ' s',
's.target_id = n.id AND s.target_type = \'note\''
)
->join(
'inner',
$this->table_share_link_permission . ' p',
'p.share_id = s.share_id'
)
# Is it useful to know the permission assigned?
->field('p.permission')
# We don't want get back own created note
->where('AND', 'n.creator', 'NOT IN', $params['user_id'], 'uuid');
;
$identity_id = $params['user_id'];
# Handling of group sharing
if (isset($params['user_group_id']) /*&& count($params['user_group_id'])*/) {
if (!is_array($params['user_group_id'])) {
$params['user_group_id'] = array($params['user_group_uuid']);
}
$identity_id = array_merge($identity_id, $params['user_group_id']);
}
$q->where('AND', 'p.identity_id', 'IN', $identity_id, 'id');
} else {
$q->where('AND', 'n.creator', 'IN', $params['user_id'], 'id');
}
}
# If string search by title
if (isset($params['find']) && $params['find']) {
$q->where('AND', 'n.title', 'LIKE', '%' . $params['find'] . '%');
}
# Handling of sorting
if (isset($params['order'])) {
if ($params['order'] == 'title') {
$orderStr = 'n.title';
} else {
$orderStr = 'n.updated_on';
}
if ($params['order'] == 'title') {
$orderStr = 'n.title';
} else {
$orderStr = 'n.updated_on';
}
$q->orderBy($orderStr);
} else {
// Default sorting
$q->orderBy('n.updated_on DESC');
}
if (isset($params['limit'])) {
$q->limit($params['limit'], isset($params['offset']) ? $params['offset'] : 0);
}
$res = $this->db->execute($q);
$notes = array();
while ($row = $res->fetchRow()) {
$notes[$row->uuid] = $this->fromRow($row);
}
return $notes;
}
The method persists the note to the database. If that's what it's supposed to do, then that's a single responsibility and the implementation is fine. You'll need to put the logic of deciding whether to insert or update somewhere, this seems as good a place as any.
Only if you ever needed to explicitly do inserts or updates without the implicit decision logic would it be worthwhile to separate those two out into different methods which can be called separately. But at the moment, keeping them in the same method simplifies the code (since the latter half is shared), so that's likely the best implementation.
Exempli gratia:
public function save(Note $note) {
if (..) {
$this->insert($note);
} else {
$this->update($note);
}
}
public function insert(Note $note) {
..
}
public function update(Note $note) {
..
}
The above would make sense if you sometimes needed to call insert or update explicitly for whatever reason. SRP is not really a reason for this separation though.
SOLID principles are applied to class-level terminology, they don't explicitly state about methods. A SRP itself states, that classes should have one reason to change, so as long as you can replace a responsibility which is wrapped into one class, you're okay.
Consider this:
$userMapper = new Mapper\MySQL();
// or
$userMapper = new Mapper\Mongo();
// or
$userMapper = new Mapper\ArangoDb();
$userService = new UserService($userMapper);
All those mappers implement one interface and serve one responsibility - they do abstract storage access for users. Therefore mappers have one reason to change since you can swap them easily.
Your case is not about the SRP generally. It's more about best-practice. Well, the best practice regarding methods states that they should do only one thing whenever possible and accept as less arguments as possible. That makes it easier to read and find bugs.
There's one more principle which is called Principle of Least Astonishment. It simply states that method names should explicitly do what their names imply.
Coming down to your code example:
The save() implies that it's all about data saving (updating existing record), not creating. By doing both insert and update there, you break the PoLA.
That's it, when you call explicitly insert() you know and expect that it will add a new record. The same about update() method - you know and expect that it will update a method, it will not create a new one.
Therefore I won't do both things in save(). If I want to update a record, I would call update(). If I want to create a record I would call insert().
This topic has been discussed a lot here, but I don't get it.
I would like to protect my routes with pivot tables (user_customer_relation, user_object_relation (...)) but I don't understand, how to apply the filter correctly.
Route::get('customer/{id}', 'CustomerController#getCustomer')->before('customer')
now I can add some values to the before filter
->before('customer:2')
How can I do this dynamically?
In the filter, I can do something like:
if(!User::hasAccessToCustomer($id)) {
App::abort(403);
}
In the hasAccessToCustomer function:
public function hasCustomer($id) {
if(in_array($id, $this->customers->lists('id'))) {
return true;
}
return false;
}
How do I pass the customer id to the filter correctly?
You can't pass a route parameter to a filter. However you can access route parameters from pretty much everywhere in the app using Route::input():
$id = Route::input('id');
Optimizations
public function hasCustomer($id) {
if($this->customers()->find($id)){
return true;
}
return false;
}
Or actually even
public function hasCustomer($id) {
return !! $this->customers()->find($id)
}
(The double !! will cast the null / Customer result as a boolean)
Generic approach
Here's a possible, more generic approach to the problem: (It's not tested though)
Route::filter('id_in_related', function($route, $request, $relationName){
$user = Auth::user();
if(!$user->{$relationName}()->find($route->parameter('id')){
App::abort(403);
}
});
And here's how you would use it:
->before('id_in_related:customers')
->before('id_in_related:objects')
// and so on
Just wondering if it is necessary to use else {return false;} in my codeigniter model functions or if () {} is enough and it returns false by default in case of failure?
controller:
if ($this->model_a->did()) {
$data["results"] = $this->model_a->did();
echo json_encode($data);
}
model:
public function did()
{
//some code here
if ($query && $query->num_rows() > 0) {
return $query->result_array();
} else {
return false;
}
}
in your controller -- test the negative condition first - if nothing came back from the method in your model
if ( ! $data["results"] = $this->model_a->did() ) {
$this->showNoResults() ; }
else { echo json_encode($data); }
so thats saying - if nothing came back - then go to the showNoResults() method.
If results did come back then its assigned to $data
However - in this situation in the model i would also put ELSE return false - some people would say its extra code but for me it makes it clearer what is happening. Versus methods that always return some value.
I think this is more of a PHP question than a CodeIgniter question. You could easily test this by calling your model methods and var_dump-ing the result. If you return nothing from a method in PHP, the return value is NULL.
As much i have experience in CI returning false is not a plus point, because if you return false here then you need to have a condition back in controller which is useless you should be doing like this will save you at least some code of lines
if ($query && $query->num_rows() > 0) {
return $query->result_array();
} else {
return array();
}
so returning an array will save you from many other errors, like type error.
I'm just getting started with dependency injection and I have immediately hit a problem: I have two classes that depend on each other.
The classes are Basket and Shipping.
In my Basket class I have the following relevant methods:
public function totalShipping()
{
return $this->_shipping->rate();
}
public function grandTotal()
{
return $this->totalProductsPrice() + $this->totalShipping();
}
public function totalWeight()
{
$weight = 0;
$products = $this->listProducts();
foreach ($products as $product) {
$weight += $product['product_weight'];
}
return ($weight == '') ? 0 : $weight;
}
$this->_shipping is an instance of the Shipping class
In my Shipping class I have the following relevant methods:
public function rate()
{
if (isset($_SESSION['shipping']['method_id'])) {
$methodId = $_SESSION['shipping']['method_id'];
return $this->_rates[$methodId]['Shipping Price'];
}
// Method not set
return NULL;
}
// Available Methods depend on country and the total weight of products added to the customer's basket. E.g. USA and over 10kg
public function listAvailableMethods()
{
$rates = array();
if (isset($_SESSION['customer']['shipping_address']['country_code'])) {
foreach ($this->_rates as $method_id => $rate) {
if (($_SESSION['customer']['shipping_address']['country_code'] == $rate['Country']) && ($this->_basket->totalWeight() > $rate['Weight From']) && ($this->_basket->totalWeight() < $rate['Weight To'])) {
$rates[$method_id] = $rate;
}
}
}
return $rates;
}
$this->_basket is an instance of the Basket class.
I am totally clueless as to how to resolve this circular dependency. Thank you for your help in advance.
Update
In my Shipping Class I also have this method:
public function setMethod($method_id)
{
// A check to make sure that the method_id is one of the provided methods
if ( !array_key_exists($method_id, $this->listAvailableMethods()) ) return false;
$_SESSION['shipping'] = array(
'method_id' => $method_id
);
}
I have ended up renaming Shipping to Shipping_Methods and I have created a new class called Customer_Shipping_Methods. Essentially Customer_Shipping_Methods could be part of the Basket class but I'd rather keep it separate.
#RyanLaBarre was totally right. I was essentially mixing methods that should have been in the Basket Class with methods in my Shipping_Methods class. Shipping_Methods should have only contained general shipping data methods which were not specific to the current session.
I think what threw me, was that Shipping_Methods sources its data from a csv file rather than a database table. Once I started seeing Shipping_Methods as just another table, it all clicked in my mind.
#rdlowrey that is very good advice. I will be putting my session globals into my controllers immediately!