PHP deep clone object - php

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);

Related

Why does my function output " the method 'forTemplate' does not exist on 'SilverStripe\View\ArrayData"

This will be my very first question on here. I hope I give you all the info you need.
I am running a personal project using silverstripe v4.8
In my template, I have a optionsetfield, which is basically just a radiofield.
The first 3 items in the options, I have hardcoded.
I wrote another function for the rest to basically get me all the names of people who can make events, loop through them, and add them as options.
When I dump my outcome, it seems to come out the way I want it:
array (size=1)
'Rick' => string 'Rick' (length=4)
But when I try to see it in my template, it gives me:
Object->__call(): the method 'forTemplate' does not exist on 'SilverStripe\View\ArrayData'
Now when I don't add the function to my Optionset, the first 3 hardcoded items work fine.
I will post my OptionsetField and the other function below.
Thank you in advance
public function createEventsFilterForm()
{
$form = Form::create();
$form = FieldList::create([
OptionsetField::create('Eventfilters')
->setTitle('')
->setSource([
'past' => 'Verlopen',
'today' => 'Vandaag',
'future' => 'Toekomst',
$this->getFirstNameOfEvents()
])
]);
return $form;
}
public function getFirstNameOfEvents()
{
$allEvents = UpcomingEvents::get();
foreach ($allEvents as $event) {
$firstName = 'NVT';
$memberProfileID = $event->MemberProfileID;
if ($memberProfileID) {
$firstName = [MemberProfile::get()->byID($memberProfileID)->FirstName];
}
$output = ArrayLib::valuekey($firstName);
return $output;
}
}
tl;dr:
SilverStripe templates cannot handle arrays. I guess the array got automatically converted to an ArrayData object.
if you want to be more explicit, you can write:
return new ArrayData([
'Name' => "FOOBAR"
]);
and then in the template:
$FirstNameOfEvent <!-- this will also cause this error, because SSViwer does not know how to render ArrayData -->
<!-- but you can access object properties, like the Name that we defined: -->
$FirstNameOfEvent.Name <!-- will output FOOBAR -->
Long explanation:
forTemplate is a called by the SSViewer when rendering objects.
Basically, it's the SilverStripe equivalent to __toString(), whenever you are trying to output a object to the browser in a SilverStripe template, SSViewer (the renderer) will call forTemplate on that object.
Let me give an example:
class Foo extends VieableData {
public $message = 'Hello World';
public function forTemplate() {
return "This is a foo object with the message '{$this->message}'";
}
}
class PageController extends ContentController {
public function MyFooObject() {
return new Foo();
}
}
so if in your Page.ss template, you call $MyFooObject it will call the function of the same name and get an object. Because it's an object, SSViewer doesn't know how to render and will call Foo->forTemplate(). Which then will result in the output This is a foo object with the message 'Hello World'
ArrayData does not have a forTemplate method, thus you get the error. There are 2 ways to get around that[1]:
subclass ArrayData and implement a forTemplate method that turns your data into a string (or DBField object) that can be output to the browser
Don't try to render ArrayData in your Template, instead access the data directly (like in the tl;dr above, so $MyArrayData.MyField)[2]
[1]: the same is true for all objects
[2]: accessing object properties directly is always possible, even if you have a forTemplate method. forTemplate is just the default what to do if you don't specify a property.
EDIT:
sorry, I partially misunderstood your question/problem.
All the stuff I said above is still true, and important to understand, but it didn't answer your question.
I thought you are calling $getFirstNameOfEvents in the template, but actually, you are using it in a DropDownField (missed that part).
The thing about the SilverStripe CMS is, it also use the same templates system as the frontend for it's own things. So DropDownField will also use SSViewer to render. So my explanation is still true, it just happens inside DropDownField.ss which is a builtin template file. It does something like this:
<select>
<% loop $Source %>
<option value="$Key">$Value</option>
<% end_loop %>
</select>
$Source here is your array ['past' => 'Verlopen', 'today' => 'Vandaag', 'future' => 'Toekomst', $this->getFirstNameOfEvents()] which is automatically converted into ArrayData objects.
Now, the problem is, it doesn't work the way you think it works:
// the result you want:
['past' => 'Verlopen', 'today' => 'Vandaag', 'future' => 'Toekomst', 'Rick' => 'Rick']
// the result your code produces:
['past' => 'Verlopen', 'today' => 'Vandaag', 'future' => 'Toekomst', 0 => ['Rick' => 'Rick']]
notice how you have an array inside an array. Because getFirstNameOfEvents returns an array.
So what you should actually do:
$source = ['past' => 'Verlopen', 'today' => 'Vandaag', 'future' => 'Toekomst'];
$source = array_merge($source, $this->getFirstNameOfEvents());
$form = FieldList::create([
OptionsetField::create('Eventfilters')
->setTitle('')
->setSource($source)
]);

Setting Properties via Methods

I am new to OOP.
I am currently working on adding data to an object to then submit it to a database. I have created a method called setData to take two arguments, and then add those arguments into the object.
Inside my class, I use this
public function setData($value1, $value2) {
$this->$value1 = $value2;
}
Where on a form submit, that function is used to store data
$sites = new AddWebsites;
$sites->setData('name', $name);
$sites->setData('page_rank', $pr);
$sites->setData('pa', $pa);
$sites->setData('da', $da);
$sites->setData('tf', $tf);
$sites->setData('cf', $cf);
$sites->setData('keywords', $keywords);
$sites->setData('notes', $notes);
This will output the following data
AddWebsites Object
(
[name] => asdf.com
[page_rank] => 5
[pa] => 15
[da] => 25
[tf] => 14
[cf] => 62
[keywords] => Array
(
[0] => kw1
[1] => kw2
[2] => kw3
[3] => kw4
[4] => kw5
)
[notes] => asdf
)
I have been told that this is wrong, and will throw errors.
I was wondering if there is a better way to achieve this, if it is actually wrong, and if there is an easier way to do this.
With error reporting enabled, I have not run across anything that tells me what I am doing is wrong.
Thanks for your time.
It's wrong in pure OOP terms because you're using PHP's (somewhat unusual) ability to add arbitrary attributes to instantiated objects via your setData method.
What you should be doing - to achieve the goals of encapsulation and data validation - is something like this :
class AddWebsites {
private $name;
private $pageRank;
// etc
// Setters
public function setName(value) {
// you can put validation logic in here
this->name = value;
}
public function setPageRank(value) {
// you can put validation logic in here
this->pageRank = value;
}
// etc
// getters
public function getName() {
return this->name;
}
public function getPageRank() {
return this->pageRank;
}
}
This is using "Getters" and "Setters".
You could however have your members as public then you wouldn't need the getters
One of things i can notice is passing field name in function parameter is not an good idea. Reason behind that is if you by mistake pass wrong field name then php will create one more field for that object.
So if you are having multiple objects of same class some will have that field some will not. This leads to inconsistency.
So I feel this is not correct thing to do as you are not suppose to create properties of class pbject dynamically.
Ideal way is to have different getter and setter functions for each field and fields should be private in scope, so that you/developer will not not able to create new fields by mistake.

Accessing the value of an object in an array

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"),
);

Dynamically discover/process PHP object

Is it possible to dynamically discover the properties of a PHP object? I have an object I want to strip and return it as a fully filled stdClass object, therefore I need to get rid of some sublevel - internal - objecttypes used in the input.
My guess is I need it to be recursive, since properties of objects in the source-object can contain objects, and so on.
Any suggestions, I'm kinda stuck? I've tried fiddling with the reflection-class, get_object_vars and casting the object to an array. All without any success to be honest..
tested and this seems to work:
<?php
class myobj {private $privatevar = 'private'; public $hello = 'hellooo';}
$obj = (object)array('one' => 1, 'two' => (object)array('sub' => (object)(array('three' => 3, 'obj' => new myobj))));
var_dump($obj);
echo "\n", json_encode($obj), "\n";
$recursive_public_vars = json_decode(json_encode($obj));
var_dump($recursive_public_vars);
You can walk through an object's (public) properties using foreach:
foreach ($object as $property => $value)
... // do stuff
if you encounter another object in there (if (is_object($value))), you would have to repeat the same thing. Ideally, this would happen in a recursive function.

Constructing Child Objects in PHP

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.

Categories