How to show dirty values when debugging entity - php

I need use _get so I just did it at User entity just for test:
protected function _getName($name)
{
return $name . ' - FOOBAR';
}
So in the view I did Debug($user), and heres the result:
'properties' => [
'id' => (int) 32,
'name' => 'Daniel Pedro', //<- Clean Value
'email' => 'daniel#gmail.com',
],
'dirty' => [],
'original' => [],
'virtual' => [],
'errors' => [],
As you can notice the property name is with the original value Daniel Pedro, so I thought I did something wrong at _getName but when I look at the input at form the value was Daniel Pedro - FOOBAR.
My question is, how can I show the mutated values at Debug?

Debug the values separately
The most simple way to check the properties with their possible muatated values, is by extracting the visible properties, something like
debug($entity->extract($entity->visibleProperties()));
This won't include the ones that have been defined as "hidden" in the $_hidden property, if you need them too, then you'll have to explicitly include them
debug($entity->extract(array_merge($entity->visibleProperties(), $entity->hidden())));
Extend the debug info
If you'd wanted to somehow include this in the debug output of entities in general, then you'll have to overwrite the EntityTrait::__debugInfo() method and add the mutated properties in there.
Simple example, in your entity class (you can create a base entity class that all your entites extend so that you have this functionality in all entities):
public function __debugInfo()
{
$info = parent::__debugInfo();
$info['propertiesIncludingPossiblyMutatedValues'] =
$this->extract(array_keys($this->_properties));
return $info;
}
Or if you'd wanted to see only the ones that have really been mutated:
public function __debugInfo()
{
$info = parent::__debugInfo();
$info['mutated'] = array_diff(
$this->extract(array_keys($this->_properties)),
$this->_properties
);
return $info;
}
This should give you a hint of how things work.

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

CakePHP 3.8 belongsTo does not persist changes to new association when association is changed

I am currently using CakePHP to serve a crud based api for some ticketing logic I wrote. I am running into an issue where I am attempting to change a belongsTo association and data within the new association and it is not persisting.
The controller doing the persisting looks like this:
<?php
class TasksController extends Cake\Controller\Controller
{
public function initialize()
{
parent::initialize();
}
public function edit(array $ids): void
{
$validationErrors = [];
$tasks = $this->Tasks->find('all')
->contain($this->setAssociations($query))
->where([$this->Model->getAlias().'.id IN' => $ids]);
foreach ($tasks as $id => $task) {
$this->Tasks->patchEntity($task, $this->request->getQuery()[$id], [
'associated' => ['Asset']
]);
}
if ($this->Tasks->saveMany($tasks)) {
$this->response = $this->response->withStatus(200);
} else {
// Handle errors
}
// Render json output for success / errors
$this->set($this->createViewVars(['entities' => $tasks], $validationErrors));
}
}
The association for an asset in the task table looks like this:
<?php
class TasksTable extends Cake\ORM\Table
{
public function initialize(array $config)
{
$this->belongsTo('Asset', [
'className' => 'Assets',
'foreignKey' => 'asset_id',
'joinType' => 'LEFT'
]);
}
}
These build rules are attached to the asset table:
<?php
class AssetsTable extends Cake\ORM\Table
{
public function buildRules(RulesChecker $rules)
{
$rules->add($rules->isUnique(['number', 'group_id'], 'The Number you selected is in use'));
$rules->add($rules->isUnique(['name', 'group_id'], 'The Name you selected is in use'));
}
}
The body of the request I am sending looks like this:
{
"421933": {
"description": "This task was edited by the api!",
"asset": {
"id": "138499",
"description": "This asset was edited by they api!",
"name": "105",
"number": "6"
}
}
}
Basically the name 105 and number 6 are being flagged as not being unique, because they are already set to those values on asset 138499. The query is is instead trying to edit name 105 and number 6 into the Asset entity that is presently associated with the Task entity (645163), which is triggering the isUnquie build rules to fail.
You can see this by printing the $tasks before the saveMany call in the above controller:
Array
(
[0] => App\Model\Entity\Task Object
(
[id] => 421933
[description] => This task was edited by the api!
.....
[asset] => App\Model\Entity\Asset Object
(
[id] => 645163
[name] => 105
[description] => This asset was edited by they api!
[number] => 6
....
)
)
)
It seems like this editing Asset 138499 as an association of Task 421933 should work as it is appears to be possible to save belongsTo associations in this fashion in the CakePHP docs, as documented here:
<?php
$data = [
'title' => 'First Post',
'user' => [
'id' => 1,
'username' => 'mark'
]
];
$articles = TableRegistry::getTableLocator()->get('Articles');
$article = $articles->newEntity($data, [
'associated' => ['Users']
]);
$articles->save($article);
Is it possible to associate a belongsTo association and edit it in the same transaction? If so how should my request or code be structured differently?
Thanks!
As hinted in the comments, you're running into a limitation of the marshaller there, for which there is no overly startightforward workaround yet (you might want to open an issue over at GitHub, maybe someone want's to take up the task of adding support for this type of marshalling/saving).
The example in the book isn't really the best to go by here, in fact I'd personally remove/change it, it would only work when mass assigning the primary key is allowed (which by default for baked entities it isn't), and it would also only work when creating new records, ie when the entity that is to be saved is marked as "new". However doing this would introduce all sorts of quirks, like validation rules would see the data as "create" (new), application rules would see the entity as "update" (not new), but not receive any of the existing/original data, same for other saving stage callbacks, which would mess up stuff like auditing or the like, basically anything where you need the new data and the original data.
Long story short, even if you could get it somewhat working like with that example, you'd just introduce other problems further down the road, so better forget about that for now. The only workaround-ish thing that comes to my mind right now, would be comparing the given asset primary key, loading the existing asset entity manually, and replacing the already loaded one before patching in your data, something along the lines of this:
foreach ($tasks as $task) {
$data = $this->request->getData($task->id);
$assetId = (int)\Cake\Utility\Hash::get($data, 'asset.id');
if ($task->asset->id !== $assetId) {
$task->asset = $this->Tasks->Asset->get($assetId);
}
$this->Tasks->patchEntity($task, $data, [
'associated' => ['Asset']
]);
}
Note that I've changed your getQuery() usage to getData(), as it seemed unlikely that you're passing that data via the query string.

Get Custom String for Key based off Values Variable Name in Class?

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.

Dynamic contructor values for unit test in php

I am currently in the process of doing some refactoring. The stuff is not done by me. I just have to deal with it.
$expected = new instance(0,0,Argument::any());
$result = $this->otherInstance->returnsInstance([]);
$this->assertEquals($expected, $result);
Instance is some kind of model, which is returned by otherInstance. The problem is that the third argument is dynamic and an integer. It can be anything. As you can see, it is mandatory for instantiation of the model. Can this be mocked somehow? How do I set up the test properly?
This does obviously not work ...
::__construct() must be of the type integer, object given
So, how do I mock this? Or how do I set up the test in such a way as to handle dynamic values? The language level is 7.1, but I want to move to 7.4 soon.
One way to work with different test scenarios is using Data Providers.
In the example below, you set some arbitrary values in the method provideModelConstructorArguments. The test testMyTest will run twice, one time for each set of values present in the data provider.
public function provideModelConstructorArguments()
{
return [
[
'argument1' => 0,
'argument2' => 1,
'argument3' => 100
],
[
'argument1' => 5,
'argument2' => 3,
'argument3' => 57
]
];
}
/**
* #dataProvider provideModelConstructorArguments
**/
public function testMyTest($argument1, $argument2, $argument3)
{
$expected = new instance($argument1, $argument2, $argument3);
//continue your test code
}

Accessing nested property on stdClass Object with string representation of the node

Given a variable that holds this string:
$property = 'parent->requestdata->inputs->firstname';
And an object:
$obj->parent->requestdata->inputs->firstname = 'Travis';
How do I access the value 'Travis' using the string? I tried this:
$obj->{$property}
But it looks for a property called 'parent->requestdata->inputs->firstname' not the property located at $obj->parent->requestdtaa->inputs->firstname`
I've tried various types of concatenation, use of var_export(), and others. I can explode it into an array and then loop the array like in this question.
But the variable '$property' can hold a value that goes 16 levels deep. And, the data I'm parsing can have hundreds of properties I need to import, so looping through and returning the value at each iteration until I get to level 16 X 100 items seems really inefficient; especially given that I know the actual location of the property at the start.
How do I get the value 'Travis' given (stdClass)$obj and (string)$property?
My initial searches didn't yield many results, however, after thinking up a broader range of search terms I found other questions on SO that addressed similar problems. I've come up with three solutions. All will work, but not all will work for everyone.
Solution 1 - Looping
Using an approach similar to the question referenced in my original question or the loop proposed by #miken32 will work.
Solution 2 - anonymous function
The string can be exploded into an array. The array can then be parsed using array_reduce() to produce the result. In my case, the working code (with a check for incorrect/non-existent property names/spellings) was this (PHP 7+):
//create object - this comes from and external API in my case, but I'll include it here
//so that others can copy and paste for testing purposes
$obj = (object)[
'parent' => (object)[
'requestdata' => (object)[
'inputs' => (object)[
'firstname' => 'Travis'
]
]
]
];
//string representing the property we want to get on the object
$property = 'parent->requestdata->inputs->firstname';
$name = array_reduce(explode('->', $property), function ($previous, $current) {
return is_numeric($current) ? ($previous[$current] ?? null) : ($previous->$current ?? null); }, $obj);
var_dump($name); //outputs Travis
see this question for potentially relevant information and the code I based my answer on.
Solution 3 - symfony property access component
In my case, it was easy to use composer to require this component. It allows access to properties on arrays and objects using simple strings. You can read about how to use it on the symfony website. The main benefit for me over the other options was the included error checking.
My code ended up looking like this:
//create object - this comes from and external API in my case, but I'll include it here
//so that others can copy and paste for testing purposes
//don't forget to include the component at the top of your class
//'use Symfony\Component\PropertyAccess\PropertyAccess;'
$obj = (object)[
'parent' => (object)[
'requestdata' => (object)[
'inputs' => (object)[
'firstname' => 'Travis'
]
]
]
];
//string representing the property we want to get on the object
//NOTE: syfony uses dot notation. I could not get standard '->' object notation to work.
$property = 'parent.requestdata.inputs.firstname';
//create symfony property access factory
$propertyAccessor = PropertyAccess::createPropertyAccessor();
//get the desired value
$name = $propertyAccessor->getValue($obj, $property);
var_dump($name); //outputs 'Travis'
All three options will work. Choose the one that works for you.
You're right that you'll have to do a loop iteration for each nested object, but you don't need to loop through "hundreds of properties" for each of them, you just access the one you're looking for:
$obj = (object)[
'parent' => (object)[
'requestdata' => (object)[
'inputs' => (object)[
'firstname' => 'Travis'
]
]
]
];
$property = "parent->requestdata->inputs->firstname";
$props = explode("->", $property);
while ($props && $obj !== null) {
$prop = array_shift($props);
$obj = $obj->$prop ?? null;
}
var_dump($obj);
Totally untested but seems like it should work and be fairly performant.

Categories