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'];
Related
<?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. ;)
In PHP 8.1, BackedEnum offer a from and tryFrom method to get an enum from a value. How can the same be achieved by non backed enums?
Example BackedEnum:
enum MainType: string
{
case Full = 'a';
case Major = 'b';
case Minor = 'c';
}
var_dump(MainType::tryFrom('a')); // MainType::Full
var_dump(MainType::tryFrom('d')); // null
However this doesn't exist for regular enums.
How would I retrieve a "normal" Enum by name, like:
enum MainType
{
case Full;
case Major;
case Minor;
}
$name = (MainType::Full)->name
var_dump(name); // (string) Full
One option I've found is to simply add a tryFromName function, accepting a string and looping over all the cases like this:
enum MainType
{
case Full;
case Major;
case Minor;
public static function tryFromName(string $name): ?static
{
foreach (static::cases() as $case) {
if ($case->name === $name) {
return $case;
}
}
return null;
}
}
$name = (MainType::Full)->name
var_dump(name); // (string) Full
var_dump(MainType::tryFromName($name)); // MainType::Full
This works, however it seams counter intuitive to enable a foreach loop going over all possibilities just to create an enum.
Therefore the question is, what is the right way to get an Enum in PHP from the name.
You can use Reflection:
trait Enum {
public static function tryFromName(string $name): ?static
{
$reflection = new ReflectionEnum(static::class);
return $reflection->hasCase($name)
? $reflection->getCase($name)->getValue()
: null;
}
}
enum Foo {
use Enum;
case ONE;
case TWO;
}
var_dump( Foo::tryFromName('TWO') ); // enum(Foo::TWO)
var_dump( Foo::tryFromName('THREE') ); // null
Works also for Backed Enums.
My two cents: My package https://github.com/henzeb/enumhancer does that for you, including several other things that might be useful.
In javascript I can pass an object literal to an object as a parameter and if a value does not exist I can refer to a default value by coding the following;
this.title = params.title || false;
Is there a similar way to do this with PHP?
I am new to PHP and I can't seem to find an answer and if there is not an easy solution like javascript has, it seems pure crazy to me!!
Is the best way in PHP to use a ternary operator with a function call?
isset($params['title']) ? $params['title'] : false;
Thanks
Don't look for an exact equivalent, because PHP's boolean operators and array access mechanism are just too different to provide that. What you want is to provide default values for an argument:
function foo(array $params) {
$params += array('title' => false, ...);
echo $params['title'];
}
somethig like this $title = (isset($title) && $title !== '') ? $title : false;
Or using the empty function:
empty($params['title']) ? false : $params['title'];
$x = ($myvalue == 99) ? "x is 99": "x is not 99";
PHP one liner if ...
if ($myvalue == 99) {x is 99} else {x is not 99 //set value to false here}
<?php
class MyObject {
// Default value of object property
public $_title = null;
// Default value of argument (constructor)
public function __construct($title = null){
$this->_title = $title;
}
// Default value of argument (setter)
public function setTitle($title = null){
// Always validate arguments if you're serious about what you're doing
if(!is_null($title) and !is_string($title)){
trigger_error('$title should be a null or a string.', E_USER_WARNING);
return false;
}
$this->_title = $title;
return true;
}
} // class MyObject;
?>
This is how you do an object with default values. 3 ways in 1. You either default the property value in the class definition. Or you default it on the __construct assignment or in a specific setter setTitle.
But it all depends on the rest of your code. You need to forget JS in order to properly use PHP. This is a slightly stricter programming environment, even if very loose-typed. We have real classes in PHP, not imaginary function classes elephants that offer no IDE code-completion support like in JS.
I am checking the type of optional parameters in PHP like this:
/**
* Get players in the team using limit and
* offset.
*
*
* #param TeamInterface $participant
* #param int $limit
* #param int $offset
* #throws \InvalidArgumentException
* #return Players of a team
*/
public function getPlayers(TeamInterface $team, $limit = null, $offset = null)
{
if (func_num_args() === 2 && !is_int($limit) ){
throw new \InvalidArgumentException(sprintf('"Limit" should be of int type, "%s" given respectively.', gettype($limit)));
}
if (func_num_args() === 3 && (!is_int($limit) || !is_int($offset))){
throw new \InvalidArgumentException(sprintf('"Limit" and "Offset" should be of int type, "%s" and "%s" given respectively.', gettype($limit), gettype($offset)));
}
//.....
}
This works but there are 2 main issues with this:
1/ If I need to check the type of 4/5 optional parameters for the same int type, the code become unnecessarily long. Any ideas how to make this piece of code more maintainable? (Maybe use only one if statement to check the same type of both $limit and $offset)
2/ getPlayers($team, 2, null) throws an exception. Is this ok knowing that the function can actually handle a null value here?
You could do a for loop with an array of args. Something like:
$args = func_get_args();
for ($i = 1; $i < 4; $i++) {
if ($args[$i] !== null and !is_int($args[$i])) {
throw ...
}
}
Of course, adjust the for conditions based on your number of arguments that need to be checked.
Or...
$args = func_get_args();
// skip first
array_shift($args);
foreach ($args as $arg) {
if ($arg !== null and !is_int($arg)) {
throw ...
}
}
For 1) I would check each variable individually and throw an exception for each:
if (!is_int($limit)){
//Throw
}
if (!is_int($offset))){
//Throw
}
This still requires an if statement for each variable but is a bit less verbose.
For 2) if null values are allowed you can change the check to be something like:
if ($offset && !is_int($offset))){
//Throw
}
Finally I wouldn't recommend checking func_num_args(). In your example code calling your function with too many arguments would bypass the validation.
PHP doesn't have type hints for scalars yet.
Redesign
When you start to take a lot of optional arguments in your function you develop code smells. Something is wrong, there is an object waiting to emerge.
Build all of your optional parameters as an Object and have a validate method on it.
I think you want a GameParameters object and have a validate method on it.
getPlayers($gameParameters) {
}
Move your validation of the parameters to that object where you can build it into each setter or have a comprehensive validate() function.
Combinatorial problem
As far as the explosion of checks goes I would build an array of errors and throw that if there are errors. This can be done with or without redesign.
if ($limit != null && !is_int($limit){
#add to the errors array
}
if ($offset != null && !is_int($offset){
#add to the errors array
}
if (errors) {
throw new \InvalidArgumentException(sprintf('"Limit" and "Offset" should be of int type, "%s" and "%s" given respectively.', gettype($limit), gettype($offset)));
}
Personally I prefer to have only one argument per function (unless the function is very simple), For example the function can take $request, and returns a tree of data $response. It makes it a bit easier to loop over and extend later:
function dostuff( $request ) {
$team = #$request['team'];
$limit = #$request['limit'];
$offset = #$request['offset'];
// ...
return $response;
}
Then for validation, you can write a set of rules at the top of the function like
// define validation rules
$rules = array( 'required' => array('team'),
'depends' => array('offset' => 'limit'),
'types' => array('offset' => 'int', 'limit' => 'int' ),
);
And centralize all your error checking in one call:
// can throw exception
argcheck( array( 'request' => $request, 'rules' => $rules ) );
This might need optimization, but the general approach helps contain bloat as you increase the complexity of the functions.
Use switch to code specific functions.
switch(gettype($limit)) {
case "integer":
//do other processing
break;
}
You cannot leave your code vulnerable like that. As for a safe solution to overcome the vulnerabilities. Create a safe list like this.
public function getPlayers(TeamInterface $team, $limit = null, $offset = null) {
$safelist = array("var1" => "TeamInterface", "var2" => "integer", "var3" => "integer");
$args = function_get_args();
$status = true;
foreach($args as $key => $var) {
if(gettype($var)!=$safelist["var".$key]) {
$status = false;
break;
}
}
if(!$status) break;
//...........
}
I have lots of code like this in my constructors:-
function __construct($params) {
$this->property = isset($params['property']) ? $params['property'] : default_val;
}
Some default values are taken from other properties, which was why I was doing this in the constructor. But I guess it could be done in a setter instead.
What are the pros and cons of this method and is there a better one?
Edit: I have some dependencies where if a property is not supplied in the $params array then the value is taken from another property, however that other property may be optional and have a default value, so the order in which properties are initialized matters.
This means that if I used getters and setters then it is not obvious which order to call them in because the dependencies are abstracted away in the getter instead of being in the constructer...
I would suggest you, to write proper getter/setter functions, which assert you the correct data-type and validations (and contain your mentioned default-value logic). Those should be used inside your constructor.
When setting multiple fields, which depend on each other, it seems to be nice to have a separate setter for this complex data. In which kind of way are they depending anyway?
e.g.:
// META-Config
protected $static_default_values = array(
"price" => 0.0,
"title" => "foobar"
// and so on
);
protected $fallback_getter = array(
"price" => "getfallback_price"
);
// Class Logic
public function __construct($params){
$this->set_properties($params);
}
public set_properties($properties){
// determines the sequence of the setter-calls
$high_prio_fields = array("price", "title", "unimportant_field");
foreach($high_prio_fields as $field){
$this->generic_set($field, $properties[$field]);
// important: unset fields in properties-param to avoid multiple calls
unset($properties[$field]);
}
foreach($properties as $field => $value){
$this->generic_set($field, $value);
}
}
// this could also be defined within the magic-setter,
// but be aware, that magic-functions can't be resolved by your IDE completely
// for code-completion!
private function generic_set($field, $value){
// check if setter exists for given field-key
$setter_func = "set_".$v;
if(method_exists($this, $setter_func){
call_user_func_array(array($this, $setter_func), array($v));
}
// else => just discard :)
}
// same comment as generic-set
private function generic_get($field){
// check if value is present in properties array
if(isset($this->properties[$field]){
return $this->properties[$field];
}
// check if fallback_getter is present
if(isset($this->fallback_getter[$field]){
return call_user_func_array(array($this, $this->fallback_getter[$field]));
}
// check for default-value in meta-config
if(isset($this->static_default_values[$field]){
return $this->static_default_values[$field];
}
// else => fail (throw exception or return NULL)
return null;
}
public function get_price(){
// custom getter, which ovverrides generic get (if you want to)
// custom code...
return $this->generic_get("price");
}
private function getfallback_price(){
return $this->properties["other_value"] * $this->properties["and_another_value"];
}
public function set_price($price){
$price = (float) $price; // convert to correct data-type
if($price >= 0.0){
$this->properties["price"] = $price;
}
// else discard setting-func, because given parameter seems to be invalid
// optional: throw exception or return FALSE on fail (so you can handle this on your own later)
}
Update to your edit:
the modified source-code should solve all your demands (order of setter-funcs, different resolvings of get-value).
Create "globally available" function array_get.
public static function array_get($array, $property, $default_value = null) {
return isset($array[$property]) ? $array[$property] : $default_value;
}
When having a lot of default options and you need to be able to overwrite them - as you have maybe seen in jQuery using .extend() before - I like to use this simple and quick method:
class Foo {
private $options;
public function __construct($override = array()) {
$defaults = array(
'param1' => 'foo',
'param2' => ...,
'paramN' => 'someOtherDefaultValue');
$this->options= array_replace_recursive($defaults, $override);
}
}
Especially for getting classes started this is a very easy and flexible way, but as already has been mentioned if that code is going to be heavily used then it probably not a bad idea to introduce some more control over those options with getters and setters, especially if you need to take actions when some of those options are get or set, like in your case dependencies if I understood your problem correctly.
Also note that you don't have to implement getters and setters yourself, in PHP you can use the __get and __set magic methods.
It follows some useless code that hopefully gives some ideas:
[...inside Foo...]
public function __set($key, $value){
switch(true){
//option exists in this class
case isset($this->options[$key]):
//below check if $value is callable
//and use those functions as "setter" handlers
//they could resolve dependencies for example
$this->options[$key] = is_callable($value) ? $value($key) : $value;
break;
//Adds a virtual setter to Foo. This so called 'magic' __set method is also called if the property doesn't exist in the class, so you can add arbitrary things.
case $key === 'someVirtualSetterProp': Xyzzy::Noop($value); break;
default:
try{ parent::__set($key, $value); } catch(Exception $e){ /* Oops, fix it! */ }
}
}
Note that in the above examples I squeezed in different approaches and it usually doesn't make sense to mix them like that. I did this only to illustrate some ideas and hopefully you will be able to decide better what suits your needs.