sending special attributes without using if statements - php

<?php
include '../classes/productsContr.cls.php';
$sku = $_POST['sku'];
$name = $_POST['name'];
$price = $_POST['price'];
$type = $_POST['type'];
$size = $_POST['size'];
$height = $_POST['height'];
$width = $_POST['width'];
$lenght = $_POST['length'];
$weight = $_POST['weight'];
$ob = new productsContr($sku,$name,$price,$type,$size,$height,$width,$lenght,$weight);
this is the addProducts.inc.php file. it takes the post request parameters and sends them to the controller file
<?php
include 'products.cls.php';
class productsContr extends Products {
private $sku;
private $name;
private $price;
private $type;
private $size;
private $height;
private $width;
private $length;
private $weight;
public function __construct($sku, $name, $price, $type, $size, $height, $width, $length, $weight) {
$this->sku = $sku;
$this->name = $name;
$this->price = $price;
$this->type = $type;
$this->size = $size;
$this->height = $height;
$this->width = $width;
$this->length = $length;
$this->weight = $weight;
}
private static $specialAttributes = [
"DVD-Disk" => [$this->size],
"Furniture" => [
$this->height,
$this->width,
$this->length,
],
"Book" => [$this->weight],
];
$ob = new Products();
$ob->addProduct(
$sku:$this->sku,
$name:$this->name,
$price:$this->price,
$size:$this->size,
$height:$this->height,
$width:$this->width,
$length:$this->length,
$weight:$this->weight
);
}
this is the productsContr.cls.php file and it should tell the model file how to add the products to the database.
<?php
include 'dbh.cls.php';
class Products extends Dbh{
public function addProduct(
$sku = '',
$name = '',
$price = 0,
$size = 0,
$height = 0,
$width = 0,
$length = 0,
$weight = 0
) {
$sql = "INSERT INTO products (product_sku, product_name, product_price, product_size, product_height, product_width, product_length, product_weight) ($sku, $name, $price, $size, $height, $width, $length, $weight);";
}
}
this is the products.cls.php model file.
with each product I want to send the sku, the name, the price, and the special attributes for each type as in the associative array $specialAtrributes, but without using if or switch statements
(this is a test task)
I thought this would work with the associative array but I am stuck right now. any solutions?

First things first
First of all your code example seems to be wide open for sql injection. Please fix this first. There are some rules when it comes to software developments. One of the most important patterns is seperate things as most as you can. Your code tries to mix up different things. That 's always not a good approach.
Seperation of concerns
Think of the terms you use. When we are speaking of products we as humans know, that products can be a furniture, a book or what so ever. Taking a deeper dive we will recognize, that a book has different attributes than a furniture. When thinking of seperation of concerns we have to seperate this two things, because they have different attributes.
Data modeling
As you can see in your own code you will always run into trouble when trying to do several things at once. First think of all attributes that all products have in common. As you already found out sku, name and price are attributes that all products have in common. Let us create an abstraction of that.
<?php
declare(strict_types=1);
namespace Marcel\Model;
abstract class Product
{
public function __construct(
protected string $sku = '',
protected string $name = '',
protected float $price = 0,
) {}
public function toArray(): array
{
return get_object_vars($this);
}
}
The abstract class above takes all properties every product has in common. From now on you 're able to inherit from that class and create all the products you want to offer.
<?php
declare(strict_types=1);
namespace Marcel\Model;
class Furniture extends Product
{
public function __construct(
protected string $sku = '',
protected string $name = '',
protected float $price = 0,
protected float $height = 0,
protected float $width = 0,
protected float $length = 0
) {}
}
class Book extends Product
{
public function __construct(
protected string $sku = '',
protected string $name = '',
protected float $price = 0,
protected float $weight = 0
) {}
}
Seems to be a bit of work. But this effort will pay off very easily later. Because with this exactly defined product models you 're able to do some fancy things like hydration. But let us take a look at your controller first.
The controller
As you might already know your controller handles all your incoming data. Because you don't know what data you are receiving, you have to be a bit more dynamic at this point. As long as you know the type of your product, our project is very simple.
<?php
declare(strict_types=1);
namespace Marcel\Controller;
use InvalidArgumentException;
use ReflectionClass;
class ProductController
{
public function addAction(): void
{
/*
[
'sku' => 'abc-123',
'name' => 'harry potter',
'price' => '99.99',
'type' => 'book',
'size' => '',
'height' => ''
'width' => '',
'length' => '',
'weight' => '350',
]
*/
if ($_POST) {
if (! isset($_POST['type'])) {
throw new InvalidArgumentException('No product type');
}
$productFqcn = '\\Marcel\\Model\\' . ucfirst($_POST['type']);
if (! class_exists($productFqcn) {
throw new InvalidArgumentException('Invalid product type');
}
// this will throw type errors because you have to sanitize your
// post data before hydration. Take care of floats and strings.
$product = new $productFqcn();
$reflector = new ReflectionClass($product);
$properties = $reflector->getProperties();
foreach ($properties as $property) {
if (isset($_POST[$property->getName()])) {
$property->setAccessible(true);
$property->setValue($product, $_POST[$property->getName()]);
}
}
// at this point $product contains all relevant properties
}
}
}
The controller above uses reflection to find out which properties your product has. A book has no height property so it won 't be hydrated. At the end the book data object contains all the needed properties.
Hydration is a common approach when it comes to data handling. As statet out in the controller you have to sanitize your post data before hydration. Just filter and validate your incoming data that it fits for the declared types in the data models.
Repositories for database handling
The same abstraction level goes for repositories, that can store the received data into the database. Because every product has different properties, you can seperate into different product database tables. If you want to store every product in just one single table watch out for columns with null as default value.
The following approach assumes one single product table.
<?php
declare(strict_types=1);
namespace Marcel\Repository;
use Marcel\Model\Product;
class ProductRepository
{
public function add(Product $product): void
{
// find your which datafields to store
$data = $product->toArray();
$sql = 'INSERT INTO products (%s) VALUES (:%s)';
$sql = sprintf(
$sql,
implode(', ', array_keys($data)),
implode(', :', array_values($data))
);
// sql prepared statement here and so on
}
}
As you can see the repository just takes a single parameter $product, which must be an instance of our product data model. Type hints lead to type safety. From now on you 're able to take any product, extract its data and insert it into a database table dynamically. The example above hints to prepared statements. Another point you have an eye on.
Let us extend the controller from above.
<?php
declare(strict_types=1);
namespace Marcel\Controller;
use InvalidArgumentException;
use Marcel\Repository\ProductRepository;
use ReflectionClass;
class ProductController
{
public function addAction(): void
{
...
// code from above
// persist a new product in the database
$repository = new ProductRepository();
$repository->add($product);
}
}
Conclusion
It is important to think about how the data is structured before programming. Get a clear picture of what data is the same and what data is not. Abstract as much as possible, because you don't want to make unnecessary work for yourself. Always follow the separation of concerns strategy. This will make your life much easier in the future.
As you can see from the code shown, it is not really easy to abstract so far that you can store all products with one controller. But it is possible. However, you will not get around some important things like input filters and prepared statements. These are elementarily important for the security of your application.
But hey ... no if or switch conditions shown in the code above, to seperate the different products. ;)

Related

PHP project without using conditional statements

There is a page, on the page there are fields for filling, these fields are intended for entering data on a certain product. Three products with their characteristics are offered for filling.
The main feature of the project is that I am forbidden to use conditional statements (if, else, switch case, ternary) for handling differences in product types.
My question is: Is there a way to accomplish this project without using conditional statements ?
In my project used conditional statements. But I need to redo it without using conditional statements.
This is the method insertData() that accepts the data entered by the user:
public function insertData(){
$sku = filter_input(INPUT_POST, 'sku');
$name = filter_input(INPUT_POST, 'name');
$price = filter_input(INPUT_POST, 'price');
$size_mb = filter_input(INPUT_POST, 'size_mb');
$book_weight = filter_input(INPUT_POST, 'b_weight');
$height = filter_input(INPUT_POST, 'height');
$width = filter_input(INPUT_POST, 'width');
$length = filter_input(INPUT_POST, 'length');
$_SESSION["sku"] = $sku;
$_SESSION["name"] = $name;
$_SESSION["price"] = $price;
$product_Validation = new Product_Validation();
$product_Validation->productValidation($sku, $name, $price, $size_mb, $book_weight, $height, $width, $length);
// if(!isset($_SESSION['mb_error'])){
// $product_Validation->insertDvd($sku, $name, $price, $size_mb);
// } elseif(!isset($_SESSION['boo_error'])){
// $product_Validation->insertBook($sku, $name, $price, $book_weight);
// } elseif(!isset($_SESSION['w_error'])){
// $product_Validation->InsertFurniture($sku, $name, $price, $height, $width, $length);
// }
!isset($_SESSION['mb_error']) ? $product_Validation->insertDvd($sku, $name, $price, $size_mb)
: (!isset($_SESSION['boo_error'])
? $product_Validation->insertBook($sku, $name, $price, $book_weight)
: (!isset($_SESSION['w_error'])
? $product_Validation->InsertFurniture($sku, $name, $price, $height, $width, $length) : die ) );
}
This is the class where I validate the input and then add each product to their tables in the database:
// In this class, input data is validated. Created three methods that send verified data to the database.
// Above ternary expressions were created and commented out conditional statements, this was done to make it easier
// for the reader of the code to understand what the code is doing.
session_start();
class Product_Validation
{
public static function redirectToaddPage(){
header("Location: http://product-app/product_add");
}
public static function redirectToMainPage(){
header("Location: http://product-app/");
}
public function productValidation($sku, $name, $price, $size_mb, $book_weight, $height, $width, $length ){
// if(empty($sku)){
// $_SESSION['sku_error']='Please insert SKU.';
// }elseif(iconv_strlen($sku)<5){
// $_SESSION['sku_error']='sku must be at least 5 characters.';
// }
$skuVal = (empty($sku)) ? $_SESSION['sku_error']='Please insert SKU.'
: ((iconv_strlen($sku)<5) ? $_SESSION['sku_error']='sku must be at least 5 characters.' : Product_Validation :: redirectToaddPage());
// if(empty($name)){
// $_SESSION['name_error']='Please insert name.';
// }elseif(iconv_strlen($name)<3){
// $_SESSION['name_error']='name must be at least 3 characters.';
// }
$nameVal = (empty($name)) ? $_SESSION['name_error']='Please insert name.'
: ((iconv_strlen($name)<3) ? $_SESSION['name_error']='name must be at least 3 characters.' : Product_Validation :: redirectToaddPage());
// if(empty($price)){
// $_SESSION['price_error']='Please insert price.';
// }elseif(preg_match( "/[^0-9,.]/", $price)){
// $_SESSION['price_error']='Only integers and rational numbers are allowed.';
// }
$priceVal = (empty($price)) ? $_SESSION['price_error']='Please insert price.'
: ((preg_match( "/[^0-9,.]/", $price)) ? $_SESSION['price_error']='Only integers and rational numbers are allowed.' : Product_Validation :: redirectToaddPage());
// // DVD input validation
// if(empty($size_mb)){
// $_SESSION['mb_error']='*Please insert DVD size.';
// }elseif(preg_match( "/[^0-9]/", $size_mb)){
// $_SESSION['mb_error']='Only integer numbers are allowed.';
// }
$sizeVal = (empty($size_mb)) ? $_SESSION['mb_error']='Please insert DVD size.'
: ((preg_match( "/[^0-9,.]/", $size_mb)) ? $_SESSION['mb_error']='Only integer numbers are allowed.' : Product_Validation :: redirectToaddPage());
// // Book input validation
// if(empty($book_weight)){
// $_SESSION['boo_error']='*Please insert Book size.';
// }elseif(preg_match( "/[^0-9,.]/", $book_weight)){
// $_SESSION['boo_error']='Only integers and rational numbers are allowed.';
// }
$weightVal = (empty($book_weight)) ? $_SESSION['boo_error']='Please insert Book size.'
: ((preg_match( "/[^0-9,.]/", $book_weight)) ? $_SESSION['boo_error']='Only integers and rational numbers are allowed.' : Product_Validation :: redirectToaddPage());
// Furniture input validation
// if(empty($height) && empty($width) && empty($length)){
// $_SESSION['h_error']='*Please insert Furniture height.';
// $_SESSION['w_error']='*Please insert Furniture width.';
// $_SESSION['l_error']='*Please insert Furniture lenght.';
// }elseif(preg_match( "/[^0-9,.]/", $height)){
// $_SESSION['h_error']='Only integers and rational numbers are allowed.';
// }elseif(preg_match( "/[^0-9,.]/", $width)){
// $_SESSION['w_error']='Only integers and rational numbers are allowed.';
// }elseif(preg_match( "/[^0-9,.]/", $length)){
// $_SESSION['l_error']='Only integers and rational numbers are allowed.';
// }
$furVal1 = (empty($height)) ? $_SESSION['h_error']='*Please insert Furniture height.'
: ((preg_match( "/[^0-9,.]/", $height)) ? $_SESSION['h_error']='Only integers and rational numbers are allowed.': Product_Validation :: redirectToaddPage());
$furVal2 = (empty($width)) ? $_SESSION['w_error']='*Please insert Furniture width.'
: ((preg_match( "/[^0-9,.]/", $width)) ? $_SESSION['w_error']='Only integers and rational numbers are allowed.': Product_Validation :: redirectToaddPage());
$furVal3 = (empty($length)) ? $_SESSION['l_error']='*Please insert Furniture lenght.'
: ((preg_match( "/[^0-9,.]/", $length)) ? $_SESSION['w_error']='Only integers and rational numbers are allowed.': Product_Validation :: redirectToaddPage());
Product_Validation :: redirectToaddPage();
}
// If a session with certain names is not created, then the entered data is considered valid and is sent to the database
public function insertDvd($sku, $name, $price, $size_mb){
$dvdInsert = !isset($_SESSION['sku_error']) && !isset($_SESSION['name_error']) && !isset($_SESSION['price_error']) && !isset($_SESSION['mb_error'])
? $InsertDvd = new Product_Dvd() : die();
!isset($_SESSION['sku_error']) ? $InsertDvd->setSku($sku) : null;
!isset($_SESSION['name_error']) ? $InsertDvd->setName($name) : null;
!isset($_SESSION['price_error']) ? $InsertDvd->setPrice($price) : null;
!isset($_SESSION['mb_error']) ? $InsertDvd->setSize($size_mb) : null;
// $InsertDvd->insertProducts() ? header("Location: http://product-app/") : die();
$InsertDvd->insertProducts() ? Product_Validation :: redirectToMainPage() : die();
}
public function insertBook($sku, $name, $price, $book_weight){
$bookInsert = !isset($_SESSION['sku_error']) && !isset($_SESSION['name_error']) && !isset($_SESSION['price_error']) && !isset($_SESSION['boo_error'])
? $InsertBook = new Product_Book() : die();
!isset($_SESSION['sku_error']) ? $InsertBook->setSku($sku) : null;
!isset($_SESSION['name_error']) ? $InsertBook->setName($name) : null;
!isset($_SESSION['price_error']) ? $InsertBook->setPrice($price) : null;
!isset($_SESSION['boo_error']) ? $InsertBook->setWeight($book_weight) : null;
$InsertBook->insertProducts() ? Product_Validation :: redirectToMainPage() : die();
}
public function insertFurniture($sku, $name, $price, $height, $width, $length){
$bookInsert = !isset($_SESSION['sku_error']) && !isset($_SESSION['name_error']) && !isset($_SESSION['price_error']) && !isset($_SESSION['h_error'])
&& !isset($_SESSION['w_error']) && !isset($_SESSION['l_error'])
? $InsertFurniture = new Product_Furniture() : die();
!isset($_SESSION['sku_error']) ? $InsertFurniture->setSku($sku) : null;
!isset($_SESSION['name_error']) ? $InsertFurniture->setName($name) : null;
!isset($_SESSION['price_error']) ? $InsertFurniture->setPrice($price) : null;
!isset($_SESSION['h_error']) ? $InsertFurniture->setHeight($height) : null;
!isset($_SESSION['w_error']) ? $InsertFurniture->setWidth($width) : null;
!isset($_SESSION['l_error']) ? $InsertFurniture->setLength($length) : null;
$InsertFurniture->insertProducts() ? Product_Validation :: redirectToMainPage() : die();
}
}
First - Don't use the session just because it's there, pass your values around. The reasons for this are many but trust me for the time being.
Second - Start by making a map of the validators/inserters:
$validators = [
'dvd' => 'DVDValidator',
'book' => 'BookValidator',
'furniture' => 'FurnitureValidator',
'none' => 'NoneValidator'
]
Then, get the validator from the array
$type = filter_input(INPUT_POST, 'type');
Then get the class from the validators array, and make it do the validation
$validatorClass = $validators[$type];
$validator = new $validatorClass();
//Having a function with many parameters is bad, next time try to use an array
$validator->validate($sku, $name, $price, $size_mb, $book_weight, $height, $width, $length);
You shouldn't have different ways of inserting your products, but if you do, apply the same strategy that you used for the validators. If you are just storing a couple extra fields that are not common between the different products, you can have a json field called extra
For example, DVD has a directory, movie category, length, ratings, etc..
$extra = [
'length' => 120,
'director' => 'Steven Spielberg',
'ratings' => 5
]
json_encode($extra)
The normal approach to use while working with this kind of scenario would be to implement the Strategy Pattern.
The implementation will vary based on your context, your needs, and how you like to work with PHP, You can combine the Factory and the Strategy Pattern or rely on some sort of automatic dependency injection (like the example will do if on some other context).
While totally removing the use of conditionals may not be achievable in this scenario because of the nature of the request, by implementing the strategy pattern you can reduce the complexity of the code.
People Interested in this question should read more about Software Design Principles, Solid Principles, and Design Patterns.
Note: I have tried to answer this question based on its content and the comments by the original OP whose objective seemed to be to reduce the needs of the conditionals for Calling the actual validation and the Insert piece of the logic.
--
A quick demonstration of how you use the Strategy Pattern for solving this problem with plain PHP using pre-instantiated strategies
// Define your product types :)
enum ProductType: string
{
case DVD = 'dvd';
}
// In this class you Determine the strategy to be used to validate a product, by its product type and then run the rules.
class ProductValidationStrategyContext
{
public function __construct(protected iterable $productValidationStrategies) {}
public function validateProduct(string $productType, array $productData): bool
{
foreach ($this->productValidationStrategies as $productValidationStrategy) {
if ($productValidationStrategy->canValidateProduct($productType)) {
return $productValidationStrategy->validateProduct($productData);
}
}
throw new LogicException('Invalid Product type provided');
}
}
// Set up an interface for your strategies.
interface ProductValidationStrategyInterface
{
public function canValidateProduct(string $productType): bool;
public function validateProduct(array $productData): bool;
}
// This class will determine if a product type of dvd has valid data.
// Dvd Product Data is flag valid as long as it has a title with more of 3 letters
class DVDProductValidationStrategy implements ProductValidationStrategyInterface
{
public function canValidateProduct(string $productType): bool
{
return $productType === ProductType::DVD->value;
}
public function validateProduct(array $productData): bool
{
//... Validation Rules goes here, you will not be able to not use conditionals unless relying on a library
// or project that runs the conditionals for you
if (!isset($productData['title']) || strlen($productData['title']) < 3) {
return false;
}
return true;
}
}
// In This class we determine which is the strategy to be used to insert the product and then insert it.
class ProductInsertStrategyContext
{
public function __construct(protected iterable $productValidationStrategies) {}
public function insertProduct(string $productType, array $productData): bool
{
foreach ($this->productValidationStrategies as $productValidationStrategy) {
if ($productValidationStrategy->canInsertProduct($productType)) {
return $productValidationStrategy->insertProduct($productData);
}
}
throw new LogicException('Invalid Product type provided');
}
}
// Set up an interface for your strategies.
interface ProductInsertStrategyInterface
{
public function canInsertProduct(string $productType): bool;
public function insertProduct(array $productData): bool;
}
class DVDProductInsertStrategy implements ProductInsertStrategyInterface
{
public function canInsertProduct(string $productType): bool
{
return $productType === ProductType::DVD->value;
}
public function insertProduct(array $productData): bool
{
// Product Insert logic goes here...
return true;
}
}
$productType = 'dvd';
$productData = [
'title' => '123'
];
$availableProductValidationStrategies = [new DVDProductValidationStrategy()];
$availableProductInsertStrategies = [new DVDProductInsertStrategy()];
$productValidationStrategyContext = new ProductValidationStrategyContext($availableProductValidationStrategies);
$isValid = $productValidationStrategyContext->validateProduct($productType, $productData);
if ($isValid === false) {
throw new Exception('Bad Request Exception');
}
$productInsertStrategyContext = new ProductInsertStrategyContext($availableProductInsertStrategies);
$productInsertStrategyContext->insertProduct($productType, $productData);
print_r(PHP_EOL);
print_r(sprintf('The Product type, %s Product has been inserted', $productType));
print_r(PHP_EOL);
In your class, you have a "master method" productValidation that does validation for all three product types. Imagine scaling that up to 30 products. Then: One method, one concern!
From your form illustration it's evident that only one type of product will be submitted at a given time. Now, you already have separate methods for insert(Book|DVD|Furniture). The logical next step is to remove the validation logic from your "bulk method" and create separate validate(Book|DVD|Furniture) methods. You should definitely not use a ton of conditional expressions within a single method to accomplish polymorphic handling!
First off, I would reduce the arguments in your productValidation method and simply pass in a single array (say, $data) with whatever possible values. These will be sorted out by your validator methods. If we must adhere to "no conditional expressions for product validation", here's a staple construct (in my books anyway) that you can use for polymorphic method calls:
public function productValidation(array $data, string $prodType) {
$validatorMethod = 'validate' . $prodType; // = validateBook, etc.
$isValid = $this->$validatorMethod($data);
if($isValid) {
$insertMethod = 'insert' . $prodType;
$this->$insertMethod($data);
}
///...
}
This approach will make it easy for you to add new product types down the road, simply by adding new methods with appropriate names. You will want to check for valid product types at the beginning of your validator, for example by listing them in a class property:
class Product_Validation
{
// Types that match your methods;
// or UI-names mapping to method-names (key/value pairs)
public array $prodTypes = ['Book', 'DVD', 'Furniture'];
...
// and in your validator master method:
if(!in_array($prodType, $this->prodTypes)) {
protest('Invalid Product Type'); // however you handle errors
}
If you have attributes common to all products (like name, price) that you want validated, delegate them into a separate validator method validateCommon() that you call before the product-specific validators. You can remove the error check logic from your insert* methods, which should only ever be called if the validation passes to begin with.
Also consider creating coherent methods for user input error handling, and remember that classes can have properties. They're quite useful e.g. for containing the object state (rather than passing all data around in return statements). On that note, consider storing your errors etc. feedback into the object's properties, and writing the them into a session in one place when you're done (if you must, but I bet you don't, especially don't abuse sessions as global variables).
In other notes, some of your ternary expressions are really not an improvement over if/elseif/else statements. Especially when they are chained, and have no brackets, it becomes extremely unreadable. (Nested ternary expressions without brackets are deprecated as of PHP 7.4.) In further best practices, a file with a class should not cause side effects (in your case, with session_start()). It's better to separate your symbols (classes, functions, traits, etc.) from your processes/operations in the interest of code reusability.
On a general note of restructuring, you might consider separating your concerns further, whether e.g. by means of a bookClass with validate and insert methods, or by having validateClass and insertClass with methods for each product type, or by whatever other design that doesn't bundle everything into one class and especially not one master method.
It depends on the conditions.
You can use associative array to avoid some if or case.
Ex:
Instead of a switch like this
switch ($i) {
case 0:
echo "i equals 0";
break;
case 1:
echo "i equals 1";
break;
case 2:
echo "i equals 2";
break;
case 'Cat':
echo "The best";
break;
}
You can write
$iValues = [0 => 'i equal 0', 1 => 'i equal 1', 2 => 'i equal 2', 'Cat' => 'The best'];
echo $iValues['Cat'];

Laravel use dynamic mutators in where clause

So i have two columns - SizeX & SizeY. For front end users I use Laravel mutator
public function getSizeAttribute(){
/**
* Set Size
*/
$size = $this->SizeX. " x ". $this->SizeY;
/**
* Return
*/
return $size;
}
To format the sizes like this SizeX x SizeY. The column Sizes does not exists because its dynamic. Is it possible to use mutators or alternative within the Elaquent model to detect that this is a dynamic attribute, and use some sort of method to convert the SizeX x SizeY to individual columns? Once the user submits the attribute Size back to laravel application for filtering?
Edit:
Right, this is the method I'm using to retrieve filtered Items
public function scopeFilteredMaterials($query,$params = array()){
/**
* Get filters
*/
$filters = array();
/**
* Extract Info
*/
if(!empty($params) && is_array($params)){
/**
* Get Available Filters
*/
$filters = $this->getAvailableFilters();
foreach ($filters as $key => $filter){
if(isset($params[$filter])){
$filters[$filter] = $params[$filter];
unset($filters[$key]);
}else{
unset($filters[$key]);
}
}
}
foreach ($filters as $key => $filter){
$query->whereIn(key ,$filter);
}
$result = $query->get();
}
This is the filters variable which holds available filters for user to see
protected $filters = array(
"Name",
"url",
"Size",
);
I'm using the above to show the specific values to the user. Once the user selects those values I'm using the same array to check against those filters and fire the query. My problem is the Size attribute is made up of two columns which I have not problem using the following Mutator and $appends variable to automatically bring the value to the user.
/**
* Get Size Attribute
*/
public function getSizeAttribute(){
/**
* Set Size
*/
$size = $this->SizeX. " x ". $this->SizeY;
/**
* Return
*/
return $size;
}
But i ca't figure out a way to convert the Size variable back to SizeX & SizeY
If you are always creating the composite variable through the accessor, then you can use a scope that parses this string. Something like this:
public function scopeSize($query, $size)
{
$sizes = explode(' x ', $size);
return $query->where('SizeX', $sizes[0])->where('SizeY', $sizes[1]);
}
You can then use this scope in the filter method or anywhere else.
If you want to create a new dynamic attribute, just create an accessor:
public function getSizesAttribute()
{
return $this->SizeX . ' x ' . $this->SizeY;
}
If you want to set SizeX and SizeY automatically in case if Sizes property exists. In this case, you can create mutators for both SizeX and SizeY. For example:
public function setSizeXAttribute($value)
{
if (!empty($this->attributes['Sizes'])) {
$this->attributes['SizeX'] = explode(' x ', $this->attributes['Sizes']))[0];
}
}
public function setSizeYAttribute($value)
{
if (!empty($this->attributes['Sizes'])) {
$this->attributes['SizeY'] = explode(' x ', $this->attributes['Sizes']))[1];
}
}
PS: This is a solution for standard snake_case properties, like size_x. I'd really recommend you to use these in Laravel. If you still want to use SizeX, you should also do this.

Symfony2 MoneyType with divisor: integer conversion leads to wrong database values

We're storing all our money related values as cents in our database (ODM but ORM will likely behave the same). We're using MoneyType to convert user facing values (12,34€) into their cents representation (1234c). The typical float precision problem arises here: due to insufficient precision there are many cases that create rounding errors that are merely visible when debugging. MoneyType will convert incoming strings to floats that may be not precise ("1765" => 1764.9999999998).
Things get bad as soon as you persist these values:
class Price {
/**
* #var int
* #MongoDB\Field(type="int")
**/
protected $cents;
}
will transform the incoming values (which are float!) like:
namespace Doctrine\ODM\MongoDB\Types;
class IntType extends Type
{
public function convertToDatabaseValue($value)
{
return $value !== null ? (integer) $value : null;
}
}
The (integer) cast will strip off the value's mantissa instead of rounding the value, effectively leading to writing wrong values into the database (1764 instead of 1765 when "1765" is internally 1764.9999999998).
Here's an unit test that should display the issue from within any Symfony2 container:
//for better debugging: set ini_set('precision', 17);
class PrecisionTest extends WebTestCase
{
private function buildForm() {
$builder = $this->getContainer()->get('form.factory')->createBuilder(FormType::class, null, []);
$form = $builder->add('money', MoneyType::class, [
'divisor' => 100
])->getForm();
return $form;
}
// high-level symptom
public function testMoneyType() {
$form = $this->buildForm();
$form->submit(['money' => '12,34']);
$data = $form->getData();
$this->assertEquals(1234, $data['money']);
$this->assertEquals(1234, (int)$data['money']);
$form = $this->buildForm();
$form->submit(['money' => '17,65']);
$data = $form->getData();
$this->assertEquals(1765, $data['money']);
$this->assertEquals(1765, (int)$data['money']); //fails: data[money] === 1764
}
//root cause
public function testParsedIntegerPrecision() {
$string = "17,65";
$transformer = new MoneyToLocalizedStringTransformer(2, false,null, 100);
$value = $transformer->reverseTransform($string);
$int = (integer) $value;
$float = (float) $value;
$this->assertEquals(1765, (float)$float);
$this->assertEquals(1765, $int); //fails: $int === 1764
}
}
Note, that this issue is not always visible! As you can see "12,34" is working well, "17,65" or "18,65" will fail.
What is the best way to work around here (in terms of Symfony Forms / Doctrine)? The NumberTransformer or MoneyType aren't supposed to return integer values - people might also want to save floats so we cannot solve the issue there. I thought about overriding the IntType in the persistence layer, effectively rounding every incoming integer value instead of casting. Another approach would be to store the field as float in MongoDB...
The basic PHP problem is discussed here.
For now I decided to go with my own MoneyType that calls "round" on integers internally.
<?php
namespace AcmeBundle\Form;
use Symfony\Component\Form\FormBuilderInterface;
class MoneyToLocalizedStringTransformer extends \Symfony\Component\Form\Extension\Core\DataTransformer\MoneyToLocalizedStringTransformer {
public function reverseTransform($value)
{
return round(parent::reverseTransform($value));
}
}
class MoneyType extends \Symfony\Component\Form\Extension\Core\Type\MoneyType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->addViewTransformer(new MoneyToLocalizedStringTransformer(
$options['scale'],
$options['grouping'],
null,
$options['divisor']
))
;
}
}
In my opinion this problem is more related to persistence layer and I would try to solve it by overriding ODM's int type:
AppBundle\Doctrine\Types\MyIntType:
use Doctrine\ODM\MongoDB\Types\IntType;
class MyIntType extends IntType
{
public function convertToDatabaseValue($value)
{
return $value !== null ? round($value) : null;
}
}
app/config/config.yml:
doctrine:
dbal:
types:
int: AppBundle\Doctrine\Types\MyIntType

Building an array from unknown object properties

i'm trying to build an array from an Object in PHP. I only want certain properties from the object but I don;t know what they will be each time. The names of the properties I need are stored in an array. Here is how my code works currently:
// Hard-coded attributes 'colour' and 'size'
while ($objVariants->next())
{
$arrVariants[] = array
(
'pid' => $objVariants->pid,
'size' => $objVariants->size,
'colour' => $objVariants->colour,
'price' => $objVariants->price
);
}
Instead of hard coding the attributes (colour and size) I want to use variables, this is because it may not always be colour and size depending on what the user has set in the CMS. For example:
$arrVariantAttr = $this->getVariantAttr(); // Get the names of the custom variants and put them in an array e.g colour, size
while ($objVariants->next())
{
$arrVariants[] = array
(
'pid' => $objVariants->pid,
foreach($arrVariantAttr as $attr)
{
$attr['name'] => $objVariants-> . $attr['name']; // Get each variant out of the object and put into an array
}
'price' => $objVariants->price
);
}
The above code doesn't work, but hopefully it illustrates what i'm trying to do. Any help would be appreciated, thank you!
You could use get_object_vars() to get all variables of an object:
$arrVariants[] = get_object_vars($objVariants);
In order to exclude specific properties from the object you could do like this:
$arrVariants = get_object_vars($objVariants);
// array containing object properties to exclude
$exclude = array('name');
// walk over array and unset keys located in the exclude array
array_walk($arrVariants, function($val,$key) use(&$arrVariants, $exclude) {
if(in_array($key, $exclude)) {
unset($arrVariants[$key]);
}
});
You could create an array in the object containing the attributes:
$objVariants->attr['pid']
You can also use magic methods to make you object array like.
It sounds like what you really want is sub-classes or a Factory pattern.
For instance you could have a basic product object
class Product {
protected $_id;
protected $_sku;
protected $_name;
...
etc.
//getters and setters
etc.
}
... and then use sub-classes to extend that product
final class Book extends Product {
private $_isbn;
private $_language;
private $_numPages;
...
etc.
public function __construct() {
parent::__construct();
}
//getters and setters
etc.
}
That way your product types have all the attributes they need and you don't need to try and run around with an "attributes" array - though your CMS needs to be able to support product types (so that if someone wants to add a new book, the fields relevant to books appear in the CMS)... it's just a slightly more OO approach to the problem.
You could then factory pattern it; something like (a really basic example):
class ProductFactory {
const TYPE_BOOK = 'Book';
const TYPE_CD = 'CD';
const TYPE_DVD = 'DVD';
...
etc.
public static function createProduct($sProductType) {
if(class_exists($sProductType)) {
return new $sProductType();
}
else {
//throw an exception
}
}
}
You can then generate new products with something like:
$oWarAndPeace = ProductFactory::createProduct('Book')
or better yet:
$oWarAndPeace = ProductFactory::createProduct(ProductFactory::TYPE_BOOK)
Try something like this:
$arrVariants[] = Array(
'pid' => $objVariants->pid,
'price' => $objVariants->price
);
while( $objVariants->next() )
{
foreach( $arrVariantAttr as $attr )
{
end($arrVariants)[$attr['name']] = $objVariants->$attr['name'];
}
}

PHP Object Validation

I'm currently working on an OO PHP application. I have a class called validation which I would like to use to check all of the data submitted is valid, however I obviously need somewhere to define the rules for each property to be checked. At the moment, I'm using arrays during the construction of a new object. eg:
$this->name = array(
'maxlength' => 10,
'minlength' => 2,
'required' => true,
'value' => $namefromparameter
)
One array for each property.
I would then call a static method from the validation class which would carry out various checks depending on the values defined in each array.
Is there a more efficient way of doing this?
Any advice appreciated.
Thanks.
I know the associative array is used commonly to configure things in PHP (it's called magic container pattern and is considered bad practice, btw), but why don't you create multiple validator classes instead, each of which able to handle one rule? Something like this:
interface IValidator {
public function validate($value);
}
$validators[] = new StringLengthValidator(2, 10);
$validators[] = new NotNollValidator();
$validators[] = new UsernameDoesNotExistValidator();
This has multiple advantages over the implementation using arrays:
You can document them (very important), phpdoc cannot parse comments for array keys.
Your code becomes typo-safe (array('reqiured' => true))
It is fully OO and does not introduce new concepts
It is more readable (although much more verbose)
The implementation of each constraint can be found intuitively (it's not in a 400-line function, but in the proper class)
EDIT: Here is a link to an answer I gave to a different question, but that is mostly applicable to this one as well.
Since using OO it would be cleaner if you used classes for validating properties. E.g.
class StringProperty
{
public $maxLength;
public $minlength;
public $required;
public $value;
function __construct($value,$maxLength,$minLength,$required)
{
$this->value = $value;
$this-> maxLength = $maxLength;
$this-> minLength = $minLength;
$this-> required = $required;
}
function isValidat()
{
// Check if it is valid
}
function getValidationErrorMessage()
{
}
}
$this->name = new StringProperty($namefromparameter,10,2,true);
if(!$this->name->isValid())
{
$validationMessage = $this->name-getValidationErrorMessage();
}
Using a class has the advantage of encapsulating logic inside of it that the array (basically a structure) does not have.
Maybe get inspired by Zend-Framework Validation.
So define a master:
class BaseValidator {
protected $msgs = array();
protected $params = array();
abstract function isValid($value);
public function __CONSTRUCT($_params) {
$this->params = $_params;
}
public function getMessages() {
// returns errors-messages
return $this->msgs;
}
}
And then build your custom validators:
class EmailValidator extends BaseValidator {
public function isValid($val=null) {
// if no value set use the params['value']
if ($val==null) {
$val = $this->params['value'];
}
// validate the value
if (strlen($val) < $this->params['maxlength']) {
$this->msgs[] = 'Length too short';
}
return count($this->msgs) > 0 ? false : true;
}
}
Finally your inital array could become something like:
$this->name = new EmailValidator(
array(
'maxlength' => 10,
'minlength' => 2,
'required' => true,
'value' => $namefromparameter,
),
),
);
validation could then be done like this:
if ($this->name->isValid()) {
echo 'everything fine';
} else {
echo 'Error: '.implode('<br/>', $this->name->getMessages());
}

Categories