I have an abstract parent class Item, from which different types of items extend: ItemTypeA, ItemTypeB, and ItemTypeC. From my database result, I have an array:
array(
'item_name' => 'This is the item name',
'item_type' => 'ItemTypeA'
);
In PHP, what's the best way to create the item? Should I do something similar to the following?
static function constructFromDatabase($result){
$type = $result['item_type'];
$item = new $type;
return $item;
}
Yes, $item = new $type; should work just fine, source
You can even do return new $result['item_type'];, for performance -and maybe clarity/simplicity- reasons, if you are not going to use the intermediate variables anywhere else.
Related
Trying to convert a procedural script to OOP.
In my procedural script I define $metadata = array() before I define variables in a foreach loop like such:
foreach ($productdata as $productinfo) {
$price = (float) $productinfo['Price'];
$regularprice = (float) $productinfo['RegularPrice'];
And then proceeded to manually input/type what I wanted the key value (_cost and _regular_cost)
$metadata[] =
[
'key' => '_cost',
'value' => $price
];
$metadata[] =
[
'key' => '_regular_cost',
'value' => $regularprice
];
Now I am trying to compact it into a class, but am uncertain how to generate these key => _{value} names.
Something I've thought to try.. could be totally off.
So the name of my class is WooCommerceController
Class WooCommerceController
{
protected $metadata;
static $metadata_keys = ['_cost', '_regular_cost'];
Then I thought about making the class function either accept an individual metadata value
public function generateMetaData(string $metadatavalue) {
or an array of values
public function generateMetaData(array $metadatavalue_array) {
but no matter which I can think of, even if I have access to the $metadata_keys static variable, I can't think of a way for the function to distinguish between for example $price and $regularprice.
The only thing I can think of is to pass it in a strict indexed way (ensure the same order of values being passed congruent with the values in WooCommerceController::$metadata_keys)..
or I thought maybe I could name my variables --- instead of $price, rename them $_cost --- and then I was just researching methods to converting variables name to string but this seems like it is more of a hackish solution
Can anyone think of a more proper solution?
Not sure how proper this is still very much learning OOP and OOP design patterns but I came up with this solution:
Class WooCommerceController
{
protected $metadata = array();
public function generateMetaData(string $metadatakeyname, string $metadatavalue) {
$this->metadata[] =
[
'key' => $metadatakeyname,
'value' => $metadatavalue
];
return $this->metadata;
}
And in context using it like this:
$metadata = $woo->generateMetaData('_cost', $price);
$metadata = $woo->generateMetaData('_regular_cost', $price);
And it appears to fill up the array in the class context (this->metadata) and also each additional statement adds to the $metadata array out of the class context.
I wish to give a list of options as an argument to a function.
The Ideal Scenario: Named Parameters
If PHP has named parameters it would be done like so:
function setOptions($title, $url, $public = true, $placeholder = "type here...") {
...
}
setOptions($title = "Hello World", $url = "example.com", $placeholder = "hi");
Unfortunately PHP does not have named parameters (please tell me if PHP7 is planned to have some as a comment).
The solution everyone else is using: Associative Array
Most PHP scripts I have seen use an alternative array approach like so:
function setOptions($options) {
...
}
setOptions(array(
'title' => "Hello World",
'url' => "example.com",
'placeholder' => "hi"
));
Drawbacks of Associative Array Approach
Although this works fine, there are the following drawbacks:
The user does not benefit from autocompletion (taking a long time to write)
The user can easily makes mistakes in spellings
The don't know what options is available, so may frequently revert back to documentation
Is there a better way?
Is there a better way that can address these issues (either in current PHP or PHP7 or maybe even hacklang(?)).
In Hack, you can use Shapes. Shapes define a structure for associative arrays so that things can be autocompleted (depending on IDE support) and spelling mistakes are picked up by the type checker.
For instance, your example could be reworked like:
function setOptions(shape(
'title' => string,
'url' => string,
'public' => ?bool,
'placeholder' => ?string,
) $options) {
$title = $options['title'];
$url = $options['url'];
$public = Shapes::idx($options, 'public', true);
$placeholder = Shapes::idx($options, 'placeholder', 'type here...');
...
}
setOptions(shape(
'title' => 'Hello World',
'url' => 'example.com',
'placeholder' => 'hi',
));
This marks title and url to both be required options and public and placeholder are optional (all nullable types in shapes are considered to be optional). Shapes::idx is then used to get the value provided, or the default value (the third argument) if a value was not passed in.
Solution: Using fluent setters
A potential solution I have found to this problem is to use classes and fluent setters like so:
class PostOptions {
protected
$title,
$url,
$public = TRUE,
$placeholder = "type here..."; //Default Values can be set here
static function getInstance(): PostOptions {
return new self();
}
public function setTitle($title) {
$this->title = $title;
return $this;
}
public function setUrl($url) {
$this->url = $url;
return $this;
}
public function setPublic($public) {
$this->public = $public;
return $this;
}
public function setPlaceholder($placeholder) {
$this->placeholder = $placeholder;
return $this;
}
}
You can then send the options like so:
function setOptions(PostOptions $postOptions) {
//...
}
setOptions(
PostOptions::getInstance()
->setTitle("Hello World")
->setUrl("example.com")
->setPlaceholder("hi")
);
Doing it quickly! (This looks long)
Although this may look long, it can actually be implemented VERY quickly using IDE tools.
e.g. In InteliJ or PHPStorm, just type ALT+INS > Select setters > Select the fields you want to set and check the checkbox for fluent setters > click OK
Why Fluent Setters? Why Not just make all the fields public?
Using public fields is a LOT slower. This is because fluent setters can make use of chained methods, whilst the public fields way must be written like this:
$options = new PostOptions();
$options->title = "hello";
$options->placeholder = "...";
$options->url "..."
setOptions($options);
Which is a lot more typing compared to the proposed solution
Why is this better?
It's faster in IDE's when using autocomplete than the array approach
Unlikely to make mistakes in spellings (thanks to autocomplete)
Easy to see what options is available (again thanks to autocomplete)
Can give individual documentation for individual fields using PHPDoc
Can use nested options more easily e.g. If you had a list of options, and that option also had more list of options
Other OOP advantages e.g. Inheritance & Abstract Classes
How much faster is this approach?
I implemented a quick class for Wordpress labels array in: https://codex.wordpress.org/Function_Reference/register_post_type
I found that setting a property for each value (with the documentation next to you on a 2nd monitor) that the fluent setters approach is approximately 25% faster than the array approach thanks to autocomplete! However, if the documentation was not next to you, I expect this approach will far exceed 25%, as discovery of options is much quicker!
Alternative approaches are welcome
Declaration from array
This is how I normally declare my class structure. The only drawback is that it takes a while longer to write, but it allows optional parameters, defaults values, etc.
public static $defaults = array(
'user_id' => null,
'username' => null,
'avatar' => null,
'email' => null,
'description' => null,
);
public function __construct(array $args = array()) {
$this->dbc = Database::connection();
$defaults = self::$defaults;
$args = array_merge($defaults, $args);
//Assign the object properites
$this->user_id = (is_numeric($args['user_id'])) ? $args['user_id'] : null;
$this->username = $args['username'];
$this->avatar = AVATAR_DIR . $args['avatar'];
$this->email = $args['email'];
$this->description = $args['description'];
}
This way, you can declare an object like $x = new User(), and it will work perfectly fine. Let's say you've only selected a few columns from your SQL statement. You can make the keys in the public static $defaults into the same name as the columns you've selected, that way to instantiate your object, you can easily do:
$row = mysqli_fetch_array($result, MYSQLI_ASSOC);
$object = new User($row);
The array_merge takes care of having any extraneous keys that you don't need in the argument they provided. If you need to change options, you can declare them the same way for __construct() with a default array and array_merge to catch arguments and mimic named parameters and defaults values (like in Python)
With Syntactic: https://github.com/topclaudy/php-syntactic
you can just do:
function foo($a = 1, $b = 2, $c = 3, $d = 4){
return $a * $b * $c * $d;
}
And call it with the arguments you want:
//Call with argument b only
echo s('foo')->in('b', 5)->out(); //Outputs 60
//Call with argument a and argument at index/position 1 (b),
echo s('foo')->in('a', 7)->in(1, 5)->out(); //Outputs 420
//Call with argument c only through dynamic method
echo s('foo')->c(9)->out(); //Outputs 72
If U have that much parameters I'd think about creating an object that you'll pass to class instead of n parameters and every parameter is one field there. In constructor you put required parameters and this is then clean solution.
Consider the following array of objects:
class Person {
public $name;
public $occupation;
public function __construct($n, $o){
$this->name = $n;
$this->occupation = $o;
}
}
$people = array(
new Person("John", "singer"),
new Person("Paul", "guitar"),
new Person("George", "bass"),
new Person("Ringo", "drums")
);
Is there any quick way to access the objects? I wouldn't mind storing them in a different datatype (as opposed to array) if another datatype could make access easier.
Example of accessing an object: I would like to now change the "Paul" object to have an occupation of singer. This is the current solution:
foreach ( $people as &$p ) {
if ( $p->name=="Paul" )
$p->occupation="singer";
}
Alternatively, I might need to access based on a different property: Let's change all the singers' names to Yoko:
foreach ( $people as &$p ) {
if ( $p->occupation=="singer" )
$p->="Yoko";
}
Another example of accessing an object, this time in order to get the occupation of Ringo:
$ringosOccupation="";
foreach ( $people as $p ) {
if ( $p->name=="Ringo" )
$ringosOccupation = $p->occupation;
}
I suppose that I could write a People class that stores each Person object in an internal array and supplies functions to change or read occupation, but if PHP has anything cleverer build in I would love to know.
Thanks.
Just index your array with the names:
$people = array(
"John" => new Person("John", "singer"),
"Paul" => new Person("Paul", "guitar"),
"George" => new Person("George", "bass"),
"Ringo" => new Person("Ringo", "drums")
);
// Paul is the new drummer:
$people["Paul"]->occupation = "drums";
It creates a little bit of redundancy, but surely that redundancy won't be more memory or compute intensive than looping over all of them to locate the one you need every time you need to modify something.
Update:
After the question was updated, it is clear that names may be non-unique or other properties needed for access. In that case, you might be better off using a database to store object state if you have to do it often. You can't escape needing to iterate over the array if it can't be uniquely indexed. It is trivially easy to make these changes in a database, but you would need to be rebuilding the objects all the time.
So, if your array is not too large, keep looping like you have been. If it meets your performance needs, its an ok method. If you have lots and lots of these to modify, and modify often, I would suggest storing them in a database and building the objects only when you need to read one out. Then you could do:
UPDATE people SET name = 'Yoko' WHERE occupation = 'singer'
Why aren't you just setting the key of the elements to the name?
$people = array(
'john' => new Person("John", "singer"),
'paul' => new Person("Paul", "guitar"),
'george' => new Person("George", "bass"),
'ringo' => new Person("Ringo", "drums"),
);
The scenario: fetch an email template from the database, and loop through a list of recipients, personalising the email for each.
My email template is returned as a nested object. It might look a little like this:
object(stdClass) {
["title"] => "Event Notification"
["sender"] => "notifications#mysite.com"
["content"] => object(stdClass) {
["salutation"] => "Dear %%firstname%%,"
["body"] => "Lorem ipsum %%recipient_email%% etc etc..."
}
}
Then I loop through the recipients, passing this $email object to a personalise() function:
foreach( $recipients as $recipient ){
$email_body = personalise( $email, $recipient );
//send_email();
}
The issue, of course, is that I need to pass the $email object by reference in order for it to replace the personalisation tags - but if I do that, the original object is changed and no longer contains the personalisation tags.
As I understand, clone won't help me here, because it'll only create a shallow copy: the content object inside the email object won't be cloned.
I've read about getting round this with unserialize(serialize($obj)) - but everything I've read says this is a big performance hit.
So, two finally get to my two questions:
Is unserialize(serialize($obj)) a reasonable solution here?
Or am I going about this whole thing wrong? Is there a different way that I
can generate personalised copies of that email object?
You could add a __clone() method to your email class. Which is automatically called when an instance of this class is cloned via clone(). In this method you can then manually add the template.
Example:
class Email {
function __clone() {
$this->template = new Template();
}
}
.
unserialize(serialize($object)); // would be another solution...
Another more generic and powerful solution: MyCLabs\DeepCopy.
It helps creating deep copy without having to overload __clone (which can be a lot of work if you have a lot of different objects).
Recursive cloning can be done this way:
public function __clone(): void {
foreach(get_object_vars($this) as $name => $value)
if(is_object($value)) $this->{$name} = clone $value;
}
Not sure why it's not mentioned here, but the keyword clone before the object does exactly what it says.
Here's an example:
// Prepare shared stream data
$args = (object) [
'key' => $key,
'data_type' => $data_type,
'data' => clone $data_obj,
'last_update' => $last_update
];
setSharedStreamData($args);
// Object below will not change when data property
// is updated in setSharedStreamData function
print_r($data_obj);
I just ran into this building a Textbox control for my MVC framework, where just before finalizing the whole document I call PreRender on everything that inherits from ServerTag (which in turn inherits from DOMElement).
The only way i have found to change a DOMElement derived object's tagName is to replace it with a new one with all the attributes synced to the old one.
This is how that looks in code:
protected function PreRenderTextarea( WebPage $sender )
{
$textarea = $sender->createElement( 'textarea' );
foreach( $this->attributes as $attribute )
{
if ( $attribute->name == 'value' )
{
$textarea->nodeValue = $attribute->value;
}
else
{
$textarea->setAttribute( $attribute->name, $attribute->value );
}
}
$this->parentNode->replaceChild( $textarea, $this );
}
public function OnPreRender( WebPage $sender )
{
parent::OnPreRender();
$this->setAttribute( 'value', $this->Value );
switch( $this->Mode )
{
case 'normal' :
$this->setAttribute( 'type', 'text' );
break;
case 'password' :
$this->setAttribute( 'type', 'password' );
break;
case 'multiline' :
$this->PreRenderTextarea( $sender );
return;
break;
}
}
Is that really the only way to do it? This way has the rather unwanted side effect of nulling all the logic behind the control.
Yes, this how you have to do it -- the reason is that you're not just changing the value of a single attribute (tagName), you're actually changing the entire element from one type to another. Properties such as tagName (or nodeName) and nodeType are read-only in the DOM and set when you create the element.
So, creating a new element and moving in place of the old one exactly as you're doing, with DOMNode::replaceChild, is the correct operation.
I'm not sure what you mean by "unwanted side effect of nulling all the logic behind the control" -- if you clarify I might be able to give you guidance there.
It sounds like you might not want to have ServerTag inherit from DOMElement and instead you may want to link these two objects through some other pattern, such as composition (i.e. so a ServerTag "has a" DOMElement instead of "is a" DOMElement) so that you're merely replacing the DOMElement object associated with your ServerTag Textbox object.
Or a longer-shot guess is you might be running into issues just copying the attributes (i.e. textarea has required attributes, like rows and cols, that input does not).