Smarty Forum Index Smarty
WARNING: All discussion is moving to https://reddit.com/r/smarty, please go there! This forum will be closing soon.

"Dot" syntax Variable resolution (eliminating '.'
Goto page 1, 2  Next
 
This forum is locked: you cannot post, reply to, or edit topics.   This topic is locked: you cannot edit posts or make replies.    Smarty Forum Index -> Feature Requests
View previous topic :: View next topic  
Author Message
mkahn
Smarty Rookie


Joined: 05 Feb 2005
Posts: 10

PostPosted: Sat Feb 05, 2005 2:13 pm    Post subject: "Dot" syntax Variable resolution (eliminating '.' Reply with quote

If this has been discussed already, please forgive me.

One thing I would like to see in "official" Smarty (and in fact employ in my own patch) is a way for a variable expression $var.subvar.etc to employ "beans" style conventions for object-property access, to allow a template designer to navigate an object graph without having to know whether a particular element on the graph is an array or object, or if the property is accessed directly or using a "getter" method. This further abstracts the underlying model from the template, and allows the object graph to be potentially lazy-loaded at template runtime.

So, if you bound an object

Code:



class MyObject {
    var $fruit = array(
         'apple' => 'red'
     );

   function getFruit() {
        return $this->fruit;
   }
}

$smarty->assign('fruits',new MyObject());



The result of {$fruits.fruit.apple} would be 'red'.

To do this, I use the following class:

Code:

class ObjectUtils {

        function get($obj,$prop) {
                if (empty($prop)) return $obj;
                if ($prop == '..') $prop = 'parent';
               
                if (is_array($obj)) {
                        return $obj[$prop];
                }
                else if (is_object($obj)) {
                        @list($first,$last) = explode('#',$prop,2);
                        $getter = "get$first";
                        if (method_exists($obj,$getter)) {
                                if ($last) {                                       
                                        return $obj->$getter($last);
                                }
                                else {                                       
                                        return $obj->$getter();                                       
                                }
                        }
                        else {
                                if ($last){
                                        if (is_array($obj->$first)) {                                       
                                                return $obj->$first[$last];
                                        }
                                        else {
                                                throw new Exception("$first prop is not an array");
                                        }
                                }
                                else {
                                        return $obj->$prop;
                                }
                                 
                        }
                }
                else {
                        throw new Exception("Arg '$obj' " . get_class($obj) . " is not an object or array!");
                }               
        }
       
        function set(&$obj,$prop,$val) {
                if (is_array($obj)) {
                        $obj[$prop] = $val;
                }
                else if (is_object($obj)) {
                        $setter = "set$prop";
                        if (method_exists($obj,$setter)) {
                                $obj->$setter($val);
                        }
                        else {
                                $obj->$prop = $val;
                        }
                }
                else {
                        throw new Exception("Arg is not an object or array!");
                }
        }
}



The utility class will attempt to access a property first by a "getter" method, and then by direct property access, unless the argument is an array, in which case the property is used as a key.

I include this class at the top of the Smarty.class.php file. Also, I modified the smarty compiler _parse_var method as follows (note the use of ObjectUtils::get(..) instead of -> and []:

Code:


    /**
     * parse variable expression into PHP code
     *
     * @param string $var_expr
     * @param string $output
     * @return string
     */
    function _parse_var($var_expr)
    {
            // ... buncha stuff clipped to make this easier to read ...

            foreach ($_indexes as $_index) {
                if ($_index{0} == '[') {
                    $_index = substr($_index, 1, -1);
                    if (is_numeric($_index)) {
                        $_output .= "[$_index]";
                    } elseif ($_index{0} == '$') {
                        if (strpos($_index, '.') !== false) {
                            $_output .= '[' . $this->_parse_var($_index) . ']';
                        } else {
                            $_output .= "[\$this->_tpl_vars['" . substr($_index, 1) . "']]";
                        }
                    } else {
                        $_var_parts = explode('.', $_index);
                        $_var_section = $_var_parts[0];
                        $_var_section_prop = isset($_var_parts[1]) ? $_var_parts[1] : 'index';
                        $_output .= "[\$this->_sections['$_var_section']['$_var_section_prop']]";
                    }
                } else if ($_index{0} == '.') {
                    if ($_index{1} == '$') {
                         // !!! CHANGE HERE
                        $_output = "ObjectUtils::get($_output,\$this->_tpl_vars['" . substr($_index, 2) . "')";
                        //$_output .= "[\$this->_tpl_vars['" . substr($_index, 2) . "']]";
                    }                       
                    else {
                        // !!! CHANGE HERE
                        $_output = "ObjectUtils::get($_output,'" . substr($_index, 1) . "')";
                    }                       
                } else if (substr($_index,0,2) == '->') {

            // ... buncha stuff clipped to make this easier to read ...

        return $_output;
    }




In the compiled template, you will see calls to ObjectUtils::get($var,'propname') instead of $var['propname'] or $var->propname.

Does this introduce too large a performance hit for smarty users? For many of the apps I work on, I find it an indispensable feature.
Back to top
View user's profile Send private message
boots
Administrator


Joined: 16 Apr 2003
Posts: 5611
Location: Toronto, Canada

PostPosted: Sun Feb 06, 2005 1:41 am    Post subject: Reply with quote

I've always been partial to the idea that dot-notation should be prevalent in the template syntax. I wrote some modifiers to help achieve the same. Integrating the solution into the compiler is a nice idea since it is even more seemless.

I do see a couple of questions:

1) are there any issues under PHP5 with objects that implement iterators?

2) why bother checking for getter methods? Is that something in your framework? I don't think it is necessary.

3) why use a separate class -- again, is that a framework issue?

One big problem with my method is that accesses have to be inspected at runtime. I haven't looked to closely at your implementation but I suspect that must still be partially true. Have you tested speed differences?
Back to top
View user's profile Send private message
mkahn
Smarty Rookie


Joined: 05 Feb 2005
Posts: 10

PostPosted: Sun Feb 06, 2005 1:11 pm    Post subject: Speed differences, getters, PHP5, etc. Reply with quote

Hi:

PHP5: I neglected to notice the PHP5 specific stuff in there (like the exception thrown). Indeed, I guess the enhanced iterator support is an added bonus.

Getters: I'm partial to javabeans conventions for object modeling (esp. over __get or any such things). It allows domain objects from tools like Propel (that generate getter/setter accessors) to be plugged in easily (it is actually the direct property access I'd be inclined to nix, were it not for __get tricks!).

Also, you can do stuff like this:

Code:


// ... in some class def

function getSomethingLazy() {
   if (!$this->somethingLazy) {
        $this->somethingLazy = BusinessAccessorClass::lazyLookupMethod($this);
   }
}




What this does is allow the model designer to give the template designer a "dot-notation" graph of the business objects, and they can fetch (unknowingly) extra-details or objects themselves without the controller having to pre-load the template generator. The other way to do this is with custom tags, but that puts the coupling in the controller--my approach leaves issues of coupling (and their solution) down in the model layer.

Speed differences: I have not benchmarked it. I use this toolset a lot for administration-type applications where performance is not as important as maintainability.

One option might be to put a configuration swith $smarty->enhancedDotSyntax = true. Then the performace can be tuned accordingly.


Why the separate class (with a setter):

Not necessary, the 'get' login could easily be a local method. It is because I use it elswhere, as part of a toolset called OEL (Object Expression Lib) that does stuff like:

Code:


$prop = OEL::get($object,'prop.subProp.notherSubProp');

OEL::set($object,'subComponent.prop',$value);



This is handy for mapping (for instance) fields in a database row to properties of a subcomponent of an entity (e.g. for NAME_FAMILY you want to $person->getName()->setFamilyName()). Also in a configuration-file situation. Functionality trumps performance where I use this lib as well. When it becomes an issue and must-have, it'll consider PECLing it.

Thanks for your interest!

M. Kahn
Back to top
View user's profile Send private message
mkahn
Smarty Rookie


Joined: 05 Feb 2005
Posts: 10

PostPosted: Sun Feb 13, 2005 1:50 pm    Post subject: A 'gotcha' with the above code Reply with quote

If you use the above approach for dot-syntax navigation of object graphs, you can't intermix [ ] style access. The compiled tempate will throw a fatal because PHP doesn't like FunctionReturningArray()[$index] syntax, even in PHP5.

The workaround is to use the [] indexing when you are sure of the type:
Code:

{assign var=$my.object.graph.array value=arr}

{$arr[0]}


I'll post a patch that fixes this, but it requires more significant rework of the compiler code. I don't encounter this as an issue much in practice, since I would tend to wrap array-style accessors inside a clearly-defined DTO interface of some kind.
Back to top
View user's profile Send private message
boots
Administrator


Joined: 16 Apr 2003
Posts: 5611
Location: Toronto, Canada

PostPosted: Sun Feb 13, 2005 2:06 pm    Post subject: Reply with quote

I tend to only use pre-processed data holding objects in templates and so I actually favour direct property access. I'm sure a lot that has to do with the fact that my infrastructure is all built around PHP4.

As far it being implemented, outside of ourselves, I wonder if there is really any interest in this type of idea. Thanks for the update -- I'll give it a try next time I fiddle with PHP5.
Back to top
View user's profile Send private message
mkahn
Smarty Rookie


Joined: 05 Feb 2005
Posts: 10

PostPosted: Sat May 21, 2005 11:56 am    Post subject: A less intrusive SPL approach to object graph nav Reply with quote

I've been working with the PHP5 SPL and have a simpler solution that doesn't involve fiddling with the Smarty code. The above hack I posted means you would have to patch every version of Smarty, and that's no good. Short of Smarty refactoring itself to make it easier to override the internal parsing calls, simple subclassing seems unreasonable too - except by using PHP 5 and the approach below.

I wrote a custom implementation of the SPL ArrayAccess interface that does the same as the SmartyObjectNav code above:

Code:

class ObjectNavigator implements ArrayAccess {
                 
        private $wrapped = null;
       
        function __construct($wrapped) {
                $this->wrapped = $wrapped;
        }
               
        public function offsetGet($str) {
                $m = "get$str";
               
                if (method_exists($this->wrapped,$m)) {
                        return $this->wrapped->$m();
                }
                else {
                        return $this->wrapped->$str;
                }               
        }
       

        public function offsetSet($str,$value) {
                $m = "set$str";
               
                if (method_exists($this->wrapped,$m)) {
                       return $this->wrapped->$m($value);
                }
                else {
                       $this->wrapped->$str = $value;
                }
        }   

        public function offsetUnset($str) {
                $m = "unset$str";
               
                if (method_exists($this->wrapped,$m)) {
                       return $this->wrapped->$m();
                }
                else {
                       unset($this->wrapped->$str);
                }
        }
       
        public function offsetExists($a) {
                // wrong?
                return true;
        }
}


Then I created a custom Smarty subclass like this:

Code:

class MySmarty extends Smarty {

        public function assign($k, $v=null) {
                if (is_array($k)) {
                       parent::assign($k);
                }
                else {
                        if (is_object($v)) {
                                parent::assign($k, new ObjectNavigator($v));
                        }
                        else {
                                parent::assign($k, $v);
                        }
                }
        }
}



Then, you can assign an object to the template, and use {$obj.prop} style navigation, while still using the other access methods as well. Any objects assigned to the template automatically get wrapped in an ObjectNavigator, which will resolve keyed array-style access on the object by looking first for a beans-style getter method, and next for a raw property.

The real power in this approach is the ability to assign a service facade to a template, and use the template to dynamically navigate that model without having to code array "transfer objects" or anticipate what the template might want and pre-assign it.

An example:

Code:


// .. template from above

$person = $registrationSystem->getPerson($someId);

$sm->assign('person',$aPersonObject);

...

// template:
<div>
Name: {$person.commonName}

<p>We don't know if the person is a student yet, this may even lazy-load the student dependent object, and inside the foreach iterator, the associated course object and enrollments.</p>

{if $person.student}
<h3>Student details</h3>
{foreach from=$person.student.courseEnrollments item=erec}
<pre>
Course: {$erec.course.code} {$erec.course.title}
Status: {$erec.status}
</pre>
{/foreach}
{/if}
</div>


This allows the refactoring of display logic, including backing objects for that display logic, into the view where it belongs, instead of having the controller that generates the view decide what data the view might want, beyond assigning some coarse-grained domain objects with a well-defined object graph.
Back to top
View user's profile Send private message
boots
Administrator


Joined: 16 Apr 2003
Posts: 5611
Location: Toronto, Canada

PostPosted: Sat May 21, 2005 9:57 pm    Post subject: Reply with quote

Nice! I have been considering something along these very lines although I must admit that mostly I was concentrating on the iterator access for looping purposes. Personally, I think this very cool because I don't like sending application objects directly into my templates and this shows a nice technique in PHP5 for hiding and abstracting things like that. I think a lot of people could benefit from a longer exposition of this technique because I see a lot of people feeding their entire application object graphs into Smarty (sigh).

Well conceived! This idea makes enforcing contracts/interfaces that much more transparent. I'm considering creating a few Smarty "SPL" classes and this one will certainly give some inspiration on that front. One thing that I think is disappointing about Smarty is that (as a consequence of its PHP4 heritage) objects are directly assignable to the template meaning that methods are exposed without using the plugin registration features. Using something like this instead and overriding assign() to automatically wrap objects could mean that the compiler could be simplified by removing all of the object based parsing and handling it does. Obviously, this would break BC but it would return everything in the template to being registered plugins and array access and PHP5 would handle the munging of the objects to arrays in the background. I like it!
Back to top
View user's profile Send private message
mkahn
Smarty Rookie


Joined: 05 Feb 2005
Posts: 10

PostPosted: Sun May 22, 2005 4:00 pm    Post subject: Thanks, here are more details (since you're interested) Reply with quote

People copy entire object graphs into smarty (I've done it!) in an effort to abstract the access methods, because of the {$a.foo} and {$a->foo} issue, which for one requires the template designer to know way too much about the mechanics of the domain model. Not to mention you can't delegate to a getter method in the object, which is what really opens up the lazy-loading possibilities. Java/JSP figured this out early on and responded with EL and OGNL. You need to be able to hand a template designer a simplified UML class/domain diagram and away they go.

I come from a Java background, and for PHP I use a homegrown app framework that borrows from JSF and a bit from Struts: pages (templates) represent the states of the app. Backing components bound to forms and display regions respond to form-submit events and possibly initiate transitions to new states (ie: different pages). Backing components can be bound to the request or to the session. etc. The backing components are bound to the pages via a configuration file and a general-purpose naming/lookup system modeled after a mix of JNDI and Swing.

This kind of thing can't be done easily in PHP4, because of things like a lack of __autoload() and class file naming standards (I vote for the PEAR approach for the latter, since it is easy and it helps to encourage package namespaces). PHP4's shortcomings make it difficult to store full-blown objects in session from components that may or may not be able to control when session_start() is called and load their dependent classes first. PHP5 fixes this nicely.

Another feature I added to my customized, PHP5 Smarty:

$mySmarty->registerPlugin($aPluginObject, $prefix);

The registerPlugin method looks in the public methods of the plugin object for /^template_(block|modifier|function|prefilter|postfilter)_(.+)/. It registers each method it finds accordingly, optionally with a prefix to avoid namespace collisions with other plugins. I have a similar method for registering resources. With PHP5's by-reference passing of objects, you can maintain state in the plugin object, which is handy for tracking things like which backing component a form tag refers to, or fetching data from a DAO or SO. A {register_plugin name="env:plugins/myPlugin" prefix="my_"} tag mixes dynamic component lookup with the registerPlugin method to provide a JSP-taglib-on-steroids capability.
Back to top
View user's profile Send private message
mkahn
Smarty Rookie


Joined: 05 Feb 2005
Posts: 10

PostPosted: Sat May 28, 2005 12:46 pm    Post subject: Whoops Reply with quote

I neglected key element of the offsetGet() method in the ObjectNavigator (typed it from memory instead of cut-n-paste, and I'm doing the same darn thing again). If the result of an offsetGet() property access on the object returns an object itself, that object must also get wrapped in an ObjectNavigator.

Code:

class ObjectNavigator implements ArrayAccess {
                 
        private $wrapped = null;
       
        function __construct($wrapped) {
                $this->wrapped = $wrapped;
        }
               
        public function offsetGet($str) {
                $m = "get$str";
                if (is_array($this->wrapped) {
                     $ret = $this->wrapped[$str];
                }
                else if (method_exists($this->wrapped,$m)) {
                     $ret = $this->wrapped->$m();
                }
                else {
                      $ret = $this->wrapped->$str;
                }           
                if (is_array($ret) || (is_object($ret) && !($ret instanceof ArrayAccess))) {
                    return new ObjectNavigator($ret);
                }
               
                return $ret;
        }
       

        public function offsetSet($str,$value) {
                $m = "set$str";
               
                if (method_exists($this->wrapped,$m)) {
                       return $this->wrapped->$m($value);
                }
                else {
                       $this->wrapped->$str = $value;
                }
        }   

        public function offsetUnset($str) {
                $m = "unset$str";
               
                if (method_exists($this->wrapped,$m)) {
                       return $this->wrapped->$m();
                }
                else {
                       unset($this->wrapped->$str);
                }
        }
       
        public function offsetExists($a) {
                // wrong?
                return true;
        }
}



In assign:

Code:


class MySmarty extends Smarty {

        public function assign($k, $v=null) {
                if (is_array($k)) {
                       foreach ($k as $kk => $v) {
                             $this->assign($kk,$v);
                       }
                }
                else {
                        if ((is_object($v) && !($v instanceof ArrayAccess)) || is_array($v)) {
                                parent::assign($k, new ObjectNavigator($v));
                        }
                        else {
                                parent::assign($k, $v);
                        }
                }
        }
}
Back to top
View user's profile Send private message
HotHouse
Smarty n00b


Joined: 14 Jul 2005
Posts: 2

PostPosted: Thu Jul 14, 2005 11:50 pm    Post subject: Fixing debug view Reply with quote

This is a reply to the very first posting of this topic.

It worked nicely for me (PHP4), however the {debug} view wouldn't work anymore with reporting a
Code:
parse error, expecting `T_VARIABLE' or `'$''
.

The problem here is that the debug.tpl template uses if(isset($var)) and isset "only works with variables as passing anything else will result in a parse error" (from php.net)

I replaced the {if isset(..)} with simply {if ..} and that worked.

Thomas
Back to top
View user's profile Send private message
monotreme
Smarty Regular


Joined: 22 Feb 2004
Posts: 97
Location: USA

PostPosted: Thu Aug 11, 2005 2:50 pm    Post subject: Interesting Reply with quote

How much of this functionality will be brought into the PHP5 Smarty?

I'm willing to subclass, but not modify release code. Can anybody say vbulletin hacks? Razz

I'll wait for the official change, because monitoring hacks to packages is just not something we can do in my business at the moment. And now I'm subscribed to this thread. Smile
_________________
Your online 24/7 box office
http://www.tixrus.us
Back to top
View user's profile Send private message Visit poster's website
boots
Administrator


Joined: 16 Apr 2003
Posts: 5611
Location: Toronto, Canada

PostPosted: Thu Aug 11, 2005 3:32 pm    Post subject: Reply with quote

I've proposed that we consider this type of idea in the 3.x discussion thread (in the Smarty Development forum). In the meantime, if you look at mkahn's final posting, he is only extending classes -- not modifying Smarty classes directly.

Greetings.
Back to top
View user's profile Send private message
mkahn
Smarty Rookie


Joined: 05 Feb 2005
Posts: 10

PostPosted: Fri Nov 25, 2005 10:50 pm    Post subject: Late reply... Reply with quote

Actually, to be fair I ended up having to patch the compiler and several of the tags that checked for arrays in ways that still aren't SPL-friendly. I'm hoping that's something that improves with the SPL.

Thanks for pitching this idea for the Smarty 3.0 core. I think a simplified EL is crucial.
Back to top
View user's profile Send private message
boots
Administrator


Joined: 16 Apr 2003
Posts: 5611
Location: Toronto, Canada

PostPosted: Sun Nov 27, 2005 12:05 am    Post subject: Re: Late reply... Reply with quote

mkahn wrote:
Actually, to be fair I ended up having to patch the compiler and several of the tags that checked for arrays in ways that still aren't SPL-friendly. I'm hoping that's something that improves with the SPL.

Thanks for pitching this idea for the Smarty 3.0 core. I think a simplified EL is crucial.


Ok, I didn't realize that, thanks for pointing it out (although you earlier mentioned that your solution did not require touching Smarty code Wink).

Nice seeing you here again Smile -- Greetings.
Back to top
View user's profile Send private message
mkahn
Smarty Rookie


Joined: 05 Feb 2005
Posts: 10

PostPosted: Thu Mar 30, 2006 12:29 am    Post subject: Just checking up on the prospect of a simplified EL ... Reply with quote

Another late reply ... greetings likewise

I've been using a home-rolled toolkit instead of smarty. I find the simplified object-navigation EL essential (wish core PHP had it), and there were too many spots in the version of smarty I was using that were casting objects to arrays, or using one of the array functions that didn't play nice with SPL. Also, I had stuff to deploy on PHP4 that had to work without SPL.

When I mentioned that my code didn't required modification to Smarty, I was a bit too quick. Basic templates were fine, but other stuff as broken (mostly stuff in core tags that I didn't notice until I used them).

Anyway, I wrote a view framework that is basically just a plain-old PHP preprocessor that allows you to write ${a.b.c} within PHP blocks and navigate object graphs, and #{a.b.c|modifier|othermodifer:option} outside PHP blocks and do the same with an echo and modifier pipeline. Other features include more strictness with page-scoped variables; "relative" includes (relative paths are relative to the current page context, not the include_path, application root or template root); and config-file based auto-configuration.

I'm not plugging my code here, just mentioning a few of the features that motivated a roll-my-own approach.

Michael
http://www.kahn.ca
Back to top
View user's profile Send private message
Display posts from previous:   
This forum is locked: you cannot post, reply to, or edit topics.   This topic is locked: you cannot edit posts or make replies.    Smarty Forum Index -> Feature Requests All times are GMT
Goto page 1, 2  Next
Page 1 of 2

 
Jump to:  
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum


Powered by phpBB © 2001, 2005 phpBB Group
Protected by Anti-Spam ACP