So I've got a plugin which produces information for me based on a user's id.
This happens in the plugin's module 'pitch':
public function executeIndex(sfWebRequest $request)
{
$unique_id = $request->getParameter('unique_id');
$this->user = UserTable::getInstance()->getByToken($unique_id);
$this->forward404Unless($this->user);
$this->iplocation=new IPLocation();
$qualified_offers = new QualifiedOffers();
$this->creatives = $qualified_offers->applicableTo($this->user);
$this->match_type = UserDemoTable::getInstance()->getValue($this->user->id, 'match');
// Put the applicable creatives into the session for later use
$userCreatives = $this->creatives;
$this->getUser()->setAttribute('userCreatives', $userCreatives);
}
And then I try to call that attribute on the subsequent template (In a different module called 'home' with a different action):
public function executePage(sfWebRequest $request)
{
$template = $this->findTemplate($request->getParameter('view'), $this->getUser()->getCulture());
$this->forward404Unless($template);
$this->setTemplate($template);
// Grab the creatives applicable to the user
$userCreatives = $this->getUser()->getAttribute( 'userCreatives' );
}
Unfortunately it doesn't work at all.
If I try this from the action where $creatives is initially generated:
$this->getUser()->setAttribute('userCreatives', $userCreatives);
$foo = $this->getUser()->getAttribute('userCreatives');
// Yee haw
print_r($foo);
I am met with great success. I'm essentially doing this, only from two different controllers. Shouldn't that be irrelevant, given that I've added 'userCreatives' to the user's session?
It sounds like you're trying to store objects as user attributes (i.e., in the session).
From Jobeet Day 13:
You can store objects in the user session, but it is strongly discouraged. This is because the session object is serialized between requests. When the session is deserialized, the class of the stored objects must already be loaded, and that's not always the case. In addition, there can be "stalled" objects if you store Propel or Doctrine objects.
Try storing either array or stdClass representations of your objects and then loading them back into "full" objects once you retrieve them.
Here's an example that I used on another project:
class myUser extends sfGuardSecurityUser
{
...
public function setAttribute( $name, $var )
{
if( $var instanceof Doctrine_Record )
{
$var = array(
'__class' => get_class($var),
'__fields' => $var->toArray(true)
);
}
return parent::setAttribute($name, $var);
}
public function getAttribute( $name, $default )
{
$val = parent::getAttribute($name, $default);
if( is_array($val) and isset($val['__class'], $val['__fields']) )
{
$class = $val['__class'];
$fields = $val['__fields'];
$val = new $class();
$val->fromArray($fields, true)
}
return $val;
}
...
}
Related
I need to be able to create strict typed maps dynamically. Like this:
$map = new Map( 'string,array<string,int>', [
'foo' => [
'bar' => 1
]
];
I have seen a lot of solutions for separate cases. All guides are teaching to create a class for each map, like Users_Map (to keep users there), Products_Map (to keep products there), Comments_Map (to keep comments there), etc.
But I don't want to have 3 classes (dozens in fact - for a big project) for each type of the map. I want to create a single class Map and then use it like this:
$users = new Map( 'User', {users data goes here} );
$products = new Map( 'int,Product', {products data goes here} );
$comments = new Map( 'User,array<Comment>', {comments data goes here} );
I would appreciate if somebody can advice me any existing repos. Otherwise I'll probably implement this on my own and will put here a link to my solution as an answer.
What you're looking for is called generics. PHP doesn't support this, although there has been an RFC calling for support for a few years.
If you really want to enforce strict typing on a custom map, you'd have to build it yourself. You could, for example, do something like this:
class Map {
private string $keyType;
private string $valueType;
private array $items;
public function __construct(string $keyType, string $valueType) {
$this->keyType = $keyType;
$this->valueType = $valueType;
}
public function set($key, $value) {
if (gettype($key) !== $this->keyType && !($key instanceof $this->keyType)) {
throw new TypeError("Key must be of type " . $this->keyType);
}
if (gettype($value) !== $this->valueType && !($value instanceof $this->valueType)) {
throw new TypeError("Value must be of type " . $this->valueType);
}
$this->items[$key] = $value;
}
public function get($key) {
if (gettype($key) !== $this->keyType) {
throw new TypeError("Key must be of type " . $this->keyType);
}
return $this->items[$key] ?? null;
}
public function all() {
return $this->items;
}
}
(of course, this particular implementation uses a regular array internally, so keyType is limited to types that are valid array keys. If you want to support other object types, some more interesting logic might be required)
The combination of gettype and instanceof will ensure this works for both simple and complex types. For example:
$map = new Map("string", "array");
$map->set("name", ["Boris", "Johnson"]);
print_r($map->all());
/*
Array
(
[name] => Array
(
[0] => Boris
[1] => Johnson
)
)
*/
$map->set("job", "Prime Minister");
// Fatal error: Uncaught TypeError: Value must be of type array
Or with a class as value type:
class User {
public string $firstName;
public string $lastName;
}
$user = new User();
$user->firstName = "Boris";
$user->lastName = "Johnson";
$map = new Map("string", User::class);
$map->set("pm", $user);
print_r($map->all());
/*
Array
(
[pm] => User Object
(
[firstName] => Boris
[lastName] => Johnson
)
)
*/
If you also want to support nested generics, like in your example array<string,int>, that becomes more complicated. In that case, as soon as someone passes an array as a value, you'd have to manually check all items in the array to ensure all array keys are strings and all array values are integers. It's possible, but for larger arrays it will be a significant performance hit.
Although you could use a nested Map like this one if you extend it to enforce the types:
class StringIntMap extends Map {
public function __construct() {
parent::__construct("string", "integer");
}
}
$map = new Map("string", StringIntMap::class);
I created a class. The code is below
class Another {
public $error = array();
public function set_error( $key, $value )
{
if ( isset( $key ) ) {
$sanitizedKey = sanitize_key( $key );
$this->error[ $sanitizedKey ] = wp_json_encode( $value );
return $this->error;
}
}
public function get_error( $id )
{
if ( ! is_null( $id ) ) {
return $this->error[ $id ];
}
}
public function print_error()
{
if ( $this->error ) {
foreach ($this->error as $key => $value) {
$decodeJson = json_decode( $value );
?>
<div class="ud-error">
<p class="ud-error-<?php echo $key; ?>">
<?php echo __( $decodeJson, 'ud-for-edd' ); ?>
</p>
</div>
<?php
}
}
}
}
If I invoke it in the following way it works. It echos the content as expected.
$error = new Another();
$error->set_error('gg', 'hhhh');
$error->print_error();
But if I use it with function then it doesn't work as expected. Do I have to pass parameters by reference or any other? The following way it doesn't work
function create_error($id, $val) {
$errr = new Another();
return $errr->set_error($id, $val);
}
create_error('raa', 'raashid');
$error = new Another();
$error->print_error();
I am confused about why this doesn't work. Any clue. Thanks in advance.
Steps I want the code to perform:
Create a class with 3 methods, set_error, get_error, and print_error
Then invoke the class inside the function. The function will accept two parameters because inside the class the set_error() method accepts two parameters.
In order to print the error, I will instantiate the class and call the print_error() method.
In case, if I have to print the new error. I will just call the create_error() function to do this for me. Because the function needs 2 parameters. The arguments supplied to the function must be supplied as arguments to the set_error() method.
I hope the list helps.
Update:
If I use a global variable then it works. Below is working.
$customError = new Another();
function create_error($id, $val) {
global $customError;
$customError->set_error($id, $val);
}
create_error('raa', 'rashid');
$customError->print_error();
Thanks, #roggsFolly and #El_Vanja. By understanding your tips I was able to solve the error. If there is anything wrong with the code I just said worked. Please point out.
The object you instantiate inside the function is not the same one you try and print the error message from.
First the object you instantiate inside the function scope is not visible outside the function.
function create_error($id, $val) {
$errr = new Another();
return $errr->set_error($id, $val);
}
create_error('raa', 'raashid');
// this instantiates a seperate Another object from the one
// you created in the function
$error = new Another();
// this tries to print from the new object taht has no message stored in it yet
$error->print_error();
To instantiate the object inside a function scope and then use that object outside the function scope you must pass that object back to the caller of the function
function create_error($id, $val) {
$errr = new Another();
$errr->set_error($id, $val);
return $errr; // this returns the object
}
$error = create_error('raa', 'raashid');
// now you can use its methods to print the stored message
$error->print_error();
Update as per your additional Information
A couple of things I think you may be getting confused about.
Each time you do $var = new ObjectName; you are creating a brand new instance of that class. This new instance has no knowledge about any other instances of that class that may or may not have been created before or may be created after that point. And more specifically to your problems, it does not have access to the properties of another version of that object.
You are I believe missing the concept of variable scope. The Object you create inside that function, will only actually exist while the function is running. Once the function completes anything created/instantiated wholly within that function is DESTROYED ( well in truth it is just no longer accessible ) but to all intent and purpose it is destroyed. you therefore cannot expect to be able to address it outside the scope of the function.
If you want the Object you instantiate within the function to be usable outside the function, you must pass a reference to that object out of the function to the calling code. This passes that reference into the scope of the calling code and keeps the object alive, global scope in your case, but that might be another function or even another object. That allows you access to that instantiation and any properties that were set within it.
I have the following class definition:
class DatasegmentationController
{
public function indexAction()
{
$options['permissions'] = array(
'canDelete' => false,
'canEdit' => false
);
if ($this->getRequest()->isXmlHttpRequest()) {
$table = $this->getRequest()->getParam('table');
if ($table !== '' && $table !== null) {
$utilStr = new UtilString();
// This is a patch because class and tables names does not match
// so far it only happens with company and this is only for
// instantiate the proper class dynamically
$param_table = $table;
$table = $table === 'companies' ? 'company' : $table;
$classObj = strpos($table, '_') !== false ? $utilStr->stringToCamelCase($table, '_') : $utilStr->stringToCamelCase($table);
$className = new $classObj();
$module_map = $field_map[$param_table];
/** #var $module_map array */
$fields = [];
foreach ($module_map as $key => $value) {
$fields[] = [
'id' => $key,
'text' => $key
];
}
$conditions = json_decode($this->_request->getParam('conditions'), true);
$dynDataGridName = "DataSegmentation{$this->classObj}Grid";
$dynMethodName = "get{$this->classObj}GridModel";
$gridObj = new $dynDataGridName(
$this->className->$dynMethodName($conditions),
$this->view->users_id,
"{$table}_list",
"{$table}.{$table}_id",
'/datasegmentation/index',
'editor',
$options
);
return $this->_helper->json([
'fields' => $fields,
'grid' => $gridObj->getGridJs()
]);
}
if (isset($classObj, $className, $gridObj)) {
$page = $this->_request->getParam('page', 1);
$limit = $this->_request->getParam('rows', 20);
$col = $this->_request->getParam('sidx', 1);
$order = $this->_request->getParam('sord', 0);
$search = $this->_request->getParam('val', null);
echo $gridObj->getData($page, $limit, $col, $order, $search);
}
}
}
}
What the code above does is the following:
The URL http://localhost/datasegmentation is called
The view render a select element (modules) with options
When the select#modules is changed I sent it's value as part of the URL so the next AJAX call becomes: http://localhost/datasegmentation?table=companies (for example)
The indexAction() function then perform what is on the conditional for when $table is not empty or is not null
Among all those stuff it tries to generate everything dynamically as you may notice in code.
One of those stuff is a dinamic grid ($gridObj) which has a AJAX call to the same indexAction() but without parameters for populate the data after gets rendered
After the grid gets rendered at the view it makes the AJAX call and again the indexAction() is called and it skip the conditional for the table because the parameter is not set and tries the second conditional but surprise it fails because the objects that code needs to work are gone.
Having that scenario my questions are:
How do I keep the object alives between AJAX calls? Storing in a session var? Any other workaround?
If the answer is store them in a session var, is it recommendable? What about the answers on this, this and this among others?
How would you handle this?
The problem
The second AJAX call is the one adding data to the grid and it relies on the dynamic parameters. This is what I need to solve for make this to work.
I don't know if this is useful at all but this is being tested and develop on top of Zend Framework 1 project using PHP 5.5.x.
How do I keep the object alives between AJAX calls? Storing in a
session var? Any other workaround?
Storing the data in a session var
Storing the data in a file, with a client ID (could be a login, random, IP, etc)
Storing the data in a database.
If the answer is store them in a session var, is it recommendable?
What about the answers on this, this and this among others?
If you are storing critical data, use end to end encryption, SSL, HTTPS.
How would you handle this?
Using session variables.
I had many problems with has_many-through relationships but finally I found nice example here which solved most of my problems. However, according to code presented below I have couple questions.
firstly, code:
$artists = ORM::factory('artist')->find_all();
foreach ( $artists as $artist )
{
foreach ( $artist->media->find_all() as $m )
{
echo $m->name;
}
}
1) This example is probably controller. What if I want to store media in $artists to send one variable to view? Is it possible to store media as media property in artist object? (I mean for example $artists[0]->media[0]->name)
2) Is it possible to completely load $artists without this loop?
1) If I understood correctly, you need to get some element from medias
$artists = ORM::factory('artist')->find_all()->as_array();
$media = $artists[0]->media->find_all()->as_array(); // media of first artist
$name = $media[0]->name;
2) See above $artists is an array of ORM objects
Following my comment, this is what I'd do.
class Model_Artist extends ORM {
///
/// Whatever you have now
///
private $_media_cache = NULL;
public function media($key = NULL)
{
// Check cache
if($this->_media_cache == NULL)
{
$this->_media_cache = $this->media->find_all();
}
if($key !== NULL)
{
// Use Arr::get in case index does not exist
// Return empty media object when it does not exist so you can
// 'harmlessly' ask for its properties
return Arr::get($this->_media_cache, $key, ORM::factory('Media'));
}
return $this->_media_cache;
}
}
Callable as
$artists[0]->media(0)->name
So, I have a object with structure similar to below, all of which are returned to me as stdClass objects
$person->contact->phone;
$person->contact->email;
$person->contact->address->line_1;
$person->contact->address->line_2;
$person->dob->day;
$person->dob->month;
$person->dob->year;
$album->name;
$album->image->height;
$album->image->width;
$album->artist->name;
$album->artist->id;
etc... (note these examples are not linked together).
Is it possible to use variable variables to call contact->phone as a direct property of $person?
For example:
$property = 'contact->phone';
echo $person->$property;
This will not work as is and throws a E_NOTICE so I am trying to work out an alternative method to achieve this.
Any ideas?
In response to answers relating to proxy methods:
And I would except this object is from a library and am using it to populate a new object with an array map as follows:
array(
'contactPhone' => 'contact->phone',
'contactEmail' => 'contact->email'
);
and then foreaching through the map to populate the new object. I guess I could envole the mapper instead...
If i was you I would create a simple method ->property(); that returns $this->contact->phone
Is it possible to use variable variables to call contact->phone as a direct property of $person?
It's not possible to use expressions as variable variable names.
But you can always cheat:
class xyz {
function __get($name) {
if (strpos($name, "->")) {
foreach (explode("->", $name) as $name) {
$var = isset($var) ? $var->$name : $this->$name;
}
return $var;
}
else return $this->$name;
}
}
try this code
$property = $contact->phone;
echo $person->$property;
I think this is a bad thing to to as it leads to unreadable code is is plain wrong on other levels too, but in general if you need to include variables in the object syntax you should wrap it in braces so that it gets parsed first.
For example:
$property = 'contact->phone';
echo $person->{$property};
The same applies if you need to access an object that has disalowed characters in the name which can happen with SimpleXML objects regularly.
$xml->{a-disallowed-field}
If it is legal it does not mean it is also moral. And this is the main issue with PHP, yes, you can do almost whatever you can think of, but that does not make it right. Take a look at the law of demeter:
Law of Demeter
try this if you really really want to:
json_decode(json_encode($person),true);
you will be able to parse it as an array not an object but it does your job for the getting not for the setting.
EDIT:
class Adapter {
public static function adapt($data,$type) {
$vars = get_class_vars($type);
if(class_exists($type)) {
$adaptedData = new $type();
} else {
print_R($data);
throw new Exception("Class ".$type." does not exist for data ".$data);
}
$vars = array_keys($vars);
foreach($vars as $v) {
if($v) {
if(is_object($data->$v)) {
// I store the $type inside the object
$adaptedData->$v = Adapter::adapt($data->$v,$data->$v->type);
} else {
$adaptedData->$v = $data->$v;
}
}
}
return $adaptedData;
}
}
OOP is much about shielding the object's internals from the outside world. What you try to do here is provide a way to publicize the innards of the phone through the person interface. That's not nice.
If you want a convenient way to get "all" the properties, you may want to write an explicit set of convenience functions for that, maybe wrapped in another class if you like. That way you can evolve the supported utilities without having to touch (and possibly break) the core data structures:
class conv {
static function phone( $person ) {
return $person->contact->phone;
}
}
// imagine getting a Person from db
$person = getpersonfromDB();
print conv::phone( $p );
If ever you need a more specialized function, you add it to the utilities. This is imho the nices solution: separate the convenience from the core to decrease complexity, and increase maintainability/understandability.
Another way is to 'extend' the Person class with conveniences, built around the core class' innards:
class ConvPerson extends Person {
function __construct( $person ) {
Person::__construct( $person->contact, $person->name, ... );
}
function phone() { return $this->contact->phone; }
}
// imagine getting a Person from db
$person = getpersonfromDB();
$p=new ConvPerson( $person );
print $p->phone();
You could use type casting to change the object to an array.
$person = (array) $person;
echo $person['contact']['phone'];
In most cases where you have nested internal objects, it might be a good time to re-evaluate your data structures.
In the example above, person has contact and dob. The contact also contains address. Trying to access the data from the uppermost level is not uncommon when writing complex database applications. However, you might find your the best solution to this is to consolidate data up into the person class instead of trying to essentially "mine" into the internal objects.
As much as I hate saying it, you could do an eval :
foreach ($properties as $property) {
echo eval("return \$person->$property;");
}
Besides making function getPhone(){return $this->contact->phone;} you could make a magic method that would look through internal objects for requested field. Do remember that magic methods are somewhat slow though.
class Person {
private $fields = array();
//...
public function __get($name) {
if (empty($this->fields)) {
$this->fields = get_class_vars(__CLASS__);
}
//Cycle through properties and see if one of them contains requested field:
foreach ($this->fields as $propName => $default) {
if (is_object($this->$propName) && isset($this->$propName->$name)) {
return $this->$propName->$name;
}
}
return NULL;
//Or any other error handling
}
}
I have decided to scrap this whole approach and go with a more long-winded but cleaner and most probably more efficient. I wasn't too keen on this idea in the first place, and the majority has spoken on here to make my mind up for me. Thank for you for your answers.
Edit:
If you are interested:
public function __construct($data)
{
$this->_raw = $data;
}
public function getContactPhone()
{
return $this->contact->phone;
}
public function __get($name)
{
if (isset($this->$name)) {
return $this->$name;
}
if (isset($this->_raw->$name)) {
return $this->_raw->$name;
}
return null;
}
In case you use your object in a struct-like way, you can model a 'path' to the requested node explicitly. You can then 'decorate' your objects with the same retrieval code.
An example of 'retrieval only' decoration code:
function retrieve( $obj, $path ) {
$element=$obj;
foreach( $path as $step ) {
$element=$element[$step];
}
return $element;
}
function decorate( $decos, &$object ) {
foreach( $decos as $name=>$path ) {
$object[$name]=retrieve($object,$path);
}
}
$o=array(
"id"=>array("name"=>"Ben","surname"=>"Taylor"),
"contact"=>array( "phone"=>"0101010" )
);
$decorations=array(
"phone"=>array("contact","phone"),
"name"=>array("id","name")
);
// this is where the action is
decorate( $decorations, &$o);
print $o->name;
print $o->phone;
(find it on codepad)
If you know the two function's names, could you do this? (not tested)
$a = [
'contactPhone' => 'contact->phone',
'contactEmail' => 'contact->email'
];
foreach ($a as $name => $chain) {
$std = new stdClass();
list($f1, $f2) = explode('->', $chain);
echo $std->{$f1}()->{$f2}(); // This works
}
If it's not always two functions, you could hack it more to make it work. Point is, you can call chained functions using variable variables, as long as you use the bracket format.
Simplest and cleanest way I know of.
function getValueByPath($obj,$path) {
return eval('return $obj->'.$path.';');
}
Usage
echo getValueByPath($person,'contact->email');
// Returns the value of that object path