How do I create a dispatch table within a class in PHP? - php

Say I have a class with a private dispatch table.
$this->dispatch = array(
1 => $this->someFunction,
2 => $this->anotherFunction
);
If I then call
$this->dispatch[1]();
I get an error that the method is not a string. When I make it a string like this:
$this->dispatch = array(
1 => '$this->someFunction'
);
This produces
Fatal error: Call to undefined function $this->someFunction()
I have also tried using:
call_user_func(array(SomeClass,$this->dispatch[1]));
Resulting in Message: call_user_func(SomeClass::$this->someFunction) [function.call-user-func]: First argument is expected to be a valid callback.
Edit: I realized that this didn't really make sense since it is calling SomeClass::$this when $this is SomeClass. I have tried this a few ways, with the array containing
array($this, $disptach[1])
This still does not accomplish what I need.
End edit
This works if I do not have a class and just have a dispatch file with some functions. For example, this works:
$dispatch = array(
1 => someFunction,
2 => anotherFunction
);
I'm wondering if there is a way that I can still keep these as private methods in the class yet still use them with the dispatch table.

You can store the name of the method in dispatch like:
$this->dispatch = array('somemethod', 'anothermethod');
and then use:
$method = $this->dispatch[1];
$this->$method();

The call_user_func*-Family of functions should work like this:
$this->dispatch = array('somemethod', 'anothermethod');
...
call_user_func(array($this,$this->dispatch[1]));

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

php: call a method of an object coming from an array

I'm trying to figure out a way to call a method of a specified member (Class A) coming from an array of members in class B.
<?php
class A
{
public function do_something()
{
echo "class A did something";
}
}
class B
{
private $arr = array();
private $current_index = 0;
public function add_new_A()
{
$new_a = new A;
array_push($this->arr, (object) [
$this->current_index => $new_a
]);
$this->current_index++;
}
public function get_an_A_by_index($index)
{
return $this->arr{$index};
}
public function do_something_with_A_member_inside_array($index)
{
self::get_an_A_by_index($index)->do_someting();
}
}
$b = new B;
$b->add_new_a();
$b->add_new_a();
echo print_r($b->get_an_A_by_index(0));
echo "\n";
$b->do_something_with_A_member_inside_array(0); // returns error
// console:
// stdClass Object ( [0] => A Object ( ))
// Uncaught Error: Call to undefined method stdClass::do_something();
?>
To wrap things up, I want to know if my approach is considered bad and/or if there is something I can do to fix the error. Before you disagree with my code, take a look at my php code I'm actually working on. That's all for my question.
Now about why I want to call a method of a member A inside . For my assignment I want a program that does something seperately with the method do_something of class a. So far, the only way to do that is by storing seperate members of A.
I'm not sure if I'm approaching this wrong or not, because I'm coming from Java. So, The first thing I came up with was the approach shown above. When I do this in Java, it works fine. But php is different from Java and I'm still learning how php works since I'm new to it.
Your code isn't far off, you've just got an issue with the way you're building up the collection of A objects.
array_push($this->arr, (object) [
$this->current_index => $new_a
]);
This is creating a data structure that I'm pretty sure isn't what you expect. You'll end up with an array full of stdClass objects, each with a single A member and its own internal index:
Array
(
[0] => stdClass Object
(
[0] => A Object
(
)
)
)
You're then retrieving the stdClass object and trying to run the method on that, hence the Call to undefined method stdClass::do_something... error you're seeing.
Instead, all you need to do is this:
$this->arr[$this->current_index] = $new_a;
The rest of your code is just expecting an array of A objects, nothing nested any deeper.
I've put a full example here: https://3v4l.org/ijvQa. Your existing code had a couple of other typos, which are also fixed. You'll spot them easily enough if you turn on error reporting.

Is there a function to construct a callable with just a variable containing the name of the class as a string?

I am trying to construct a callable using just a string containing the name of the class that function lives in.
I have tried the following:
$class_name = "RandomInt";
// MediaWiki uses setFunctionHook()
$parser->setFunctionHook( $class_name, [constant($class_name . '::class'), 'defineParser']);
It gave me the error:
Argument 2 passed to Parser::setFunctionHook() must be callable, array given.
Here is also a working example of what it would look like if I didn't have the 'string constraint':
$parser->setFunctionHook( 'RandomInt', [RandomInt::class, 'defineParser']);
Try this sytax: $parser->setFunctionHook( $func, [ __CLASS__, $func ], Parser::SFH_NO_HASH );
Example:
$parser->setFunctionHook('defineParser', [RandomInt::class, 'defineParser']);

How to get an attribute value as Model instance in PHP ActiveRecord?

I am trying to achieve something that should be simple but seems impossible in PHP ActiveRecord.
In the simplified example I created, I have two models, Person and Exchange, and two tables, people(id, name) and exchange(id, user_from_id, user_to_id, comment). exchange.user_from_id and exchange.user_to_id have a foreign key constraint referencing people.id.
So when I run the following code:
$exchange = Exchange::first();
echo $exchange->user_from_id;
echo $exchange->user_from_id->name;
I would expect the first echo to fail and the second one to succeed. Instead, the first one succeeds and prints the litteral value of exchange.user_from_id and then obviously the second one generates a "Trying to get property of non-object" warning.
The result is the same with or without adding the following to Exchange:
static $belongs_to = array(
array('user_from_id', 'class_name' => 'Person'),
array('user_to_id', 'class_name' => 'Person')
);
What should I change in my code to make it so that $exchange->user_from_id returns an instance of the Person class?
It turns out I had misunderstood how BelongsTo work. I thought it somehow bound a property of a Model, corresponding to an attribute, to a different Model, but I realise now it creates a new property, which you then have to explicitely associate with an attribute using the foreign_key option.
Concretely, this is what I had to add to the Exchange:
static $belongs_to = array(
array('user_from',
'class_name' => 'Person',
'foreign_key' => 'user_from_id'),
array('user_to',
'class_name' => 'Person',
'foreign_key' => 'user_to_id')
);
Then, using this code:
$exchange = Exchange::first();
echo $exchange->user_from;
echo $exchange->user_from->name;
I finally got what I expected: a fatal error on the first echo and the second one printing people.name where people.id = exchange.user_from_id.
This also made it possible to use eager loading of the two people objects with:
$exchange = Exchange::first(array('include' => array('user_from', 'user_to')));

Dynamically Invoking Class Method Args

I am working on something where I need to be able to pass an indexed array of args to a method, much like how call_user_func_array works. I would use call_user_func_array but it is not an OOP approach, which is undesired, and it requires the method to be static, which breaks the target class's OO.
I have tried to use ReflectionClass but to no avail. You cannot invoke arguments to a method of the class, only the constructor. This is unfortunately, not desireable.
So I took to the man pages and looked at ReflectionFunction but there is no way to instantiate the class, point it to a method, and then invokeArgs with it.
Example using ReflectionFunction ( remember, this question is tagged PHP 5.4, hence the syntax):
$call = new \ReflectionFunction( "(ExampleClass())->exampleMethod" );
$call->invokeArgs( ["argument1", "argument2"] );
This fails with:
Function (Index())->Index() does not exist
Example using ReflectionMethod
$call = new \ReflectionMethod( "ExampleClass", "exampleMethod" );
$call->invokeArgs( new ExampleClass(), ["argument1", "argument2"] );
print_r( $call );
This fails with:
ReflectionMethod Object
(
[name] => Index
[class] => Index
)
The arguments are never passed to the method.
The desired results are:
class ExampleClass() {
public function exampleMethod( $exampleArg1, $exampleArg2 ){
// do something here
echo "Argument 1: {$exampleArg1}\n";
echo "Argument 2: {$exampleArg2}\n";
}
}
$array = [ 'exampleArg1Value', 'exampleArg2Value' ];
If I passed $array to an instance of ExampleClass->exampleMethod(), I would only have one argument, which would be an array. Instead, I need to be able to pull the individual arguments.
I was thinking that if there was a way to call ReflectorFunction on a ReflectorClass I would in in ship-shape and on my way, but it doesn't look like that is possible.
Does anyone have anything they have used to accomplish this previously?
AFAIK, the following should work:
$call = new \ReflectionMethod( "ExampleClass", "exampleMethod" );
$call->invokeArgs( new ExampleClass(), ["argument1", "argument2"] );
print_r( $call );
What minor version is PHP? Are you on 5.4.7?
I have written my own dependency injector, and I also construct classes with the parameters dynamicly. Here is some code that should get your going:
$type = 'ExampleClass';
$reflector = new \ReflectionClass( $type );
if ( !$reflector->isInstantiable() )
throw new \Exception( "Resolution target [$type] is not instantiable." );
$constructor = $reflector->getConstructor();
$parameters = $constructor->getParameters();
At this point you have a array of parameters, needed for construction. You can now substitute the parameters with the values and construct the class.
For some reason, something got stuck, somewhere.
$call = new \ReflectionMethod( "ExampleClass", "exampleMethod" );
$call->invokeArgs( new ExampleClass(), ["argument1", "argument2"] );
Now returns
Argument 1: argument1
Argument 2: argument2
I am going to try to reproduce the issue. It is on a fresh php 5.4.7 install with php-cli and fpm.

Categories