Meta-Reflection variable name detection problem for Pull Widgets in PHP

Written by AbiusX on . Posted in Computer, Development, English, Software Engineering

First of all, Happy Nowruz! Tomorrow is officially the new Jalali year's start. Best of wishes to everyone.

The Problem

I've been working on an elegant design for a new PHP Widget library for some time now. It is intended to provide a Pull MVC feature for jframework. As you know, MVC provides a separation of concerns, allowing different expertise people to work separately on their designated part of the application. The model part (which is the business logic of the application, plus some of the solution domain) is usually very re-usable and employs object oriented to great extent. The controller part, is mostly lightweight and consists of code usually not re-usable.

The problem resides in the view part of the MVC. Views are mostly HTML/CSS that dump some variables and arrays. Sometimes templates are employed in views, to reduce repeated code, but still most of the views are repeated code. Imagine two different areas of a web application, both providing tabular data of different origin. 70% of their code is the same but due to difference of data nature, usually no re-using is done.

I'm not gonna talk about benefits of code re-use here, but you get the picture.

Pull MVC, in contrast to Push MVC, is a system in which the view asks for content to render themselves by pulling them in, instead of pushing the content directly on the screen. It is usually provided by widget systems, and is much more object oriented. The problem with Pull systems is that view is usually designed and not developed, by a graphical designer who is adept in HTML and CSS.

The Actual Thingie

The first thing I wanted to do, was to preserve maximum simplicity. You can not expect a library to require redundancy to provide code re-use. I do not like libraries that take control of everything and don't let developers to change their mechanics. So I wanted to replace this practice:

$form1 = new jForm();
$form1->setName("form1");
$form1->setMethod("post");
$button1 = new jFormButton("form1");
$button1->setName("button1");
$button1->setLabel("Push Me!");

To a single liner elegant approach:

$form1 = new jForm(jForm::Method_Post);
$button1 = new jFormButton($form1, "Push Me!");

So the first thing I needed to be done, was for jForm (or any other jWidget instance) to know what variable it is assigned to - in this case $form1 - and name itself after it. The name is actually used a lot in the generated HTML code, for validation, for CSRF protection, for HTML form element names, for HTML IDs, for CSS classes and a lot more.

There were two proposed methods over the net. One was to employ get_defined_vars PHP function to get a list of variables, and then compare them with $this inside the class constructor to generate the name. It required you to code like this:

$form1 = new jForm(get_defined_vars());

Because get_defined_vars returns the list of variables in each scope. Not very elegant, but still worth a shot if it was working. There are two problems with this approach, first is that every value-equal variable (though not the same) is matched, and second something that I'll describe later.

The other approach was to get_defined_vars, change the variable and get_defined_vars again, comparing them and finding our variable. Unfortunately $this can not be changed.

The second problem, rendering both methods perfectly useless was that $form1 is not defined at the point the constructor is called, because first the right hand of the assignment is evaluated - thus calling the constructor - and then the left hand side is assigned.

The Solution

I would not let go of my desired design, as I was aware that an inconvenient library would simply not be used at all. I will post my solution first, and then describe it. The posted solution is the ParseName method of jWidget class, the base class for all other widgets used to detect their variable names:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
private function ParseName()
{
	//first lets find the file who created the widget
	$backtrace = debug_backtrace();
	$backtraceIndex=0;
	//step over all subclasses of jWidget, who called parent constructor to get the name done. Also other methods of this class
	while (isset($backtrace[$backtraceIndex]['class']) &&is_a($backtrace[$backtraceIndex]['class'],"jWidget",true)) $backtraceIndex++;
	$backtraceIndex--;
	$file=$backtrace[$backtraceIndex]['file'];
	$line=$backtrace[$backtraceIndex]['line']; //used in error messages
 
 
	$classname=get_class($this);
 
	if (isset(self::$classInstanceCountPerFile[$file]) and isset(self::$classInstanceCountPerFile[$file][$classname])) 
	//this widget already instantiated in this file, increase count and check for x-th occurance
		self::$classInstanceCountPerFile[$file][$classname]++;
	else
		self::$classInstanceCountPerFile[$file][$classname]=1;
 
	$desiredCount=self::$classInstanceCountPerFile[$file][$classname];
	$currentCount=0;
 
	$php_code = file_get_contents ( $file );
 
	$tokens = token_get_all ( $php_code );
	$count = count ( $tokens );
 
	for($i = 0; $i < $count; $i ++)
	{
		if ($tokens[$i][0]===T_NEW) //found the "new" keyword
		{
			//go forth until you find the classname
			$j=1;
			while ($tokens[$i+$j][0]==T_WHITESPACE) $j++;
			if ($tokens[$i+$j][1]==$classname) //if desired class found, increase count until we reach our desired index
				$currentCount++;
			if ($currentCount==$desiredCount) //found our variable name line, the occurance reached the desired
			{
 
				//go back until you find the assignment sign
				$j=1;
				while ($tokens[$i-$j][0]==T_WHITESPACE) $j++;
				if($tokens[$i-$j]!="=")
				{
					throw new Exception("You should instantiate jWidget by assigning it to a variable, e.g \$someWidget=new jWidget(\$this);
							in file {$file} line {$line} ");
				}
				//go furthur back until you find the variable name
				$j++;
				while ($tokens[$i-$j][0]==T_WHITESPACE) $j++;
				if($tokens[$i-$j][0]!=T_VARIABLE)
				{
					throw new Exception("Could not find variable: You should instantiate jWidget by assigning it to a variable, e.g \$someWidget=new jWidget(\$this);
							in file {$file} line {$line}");
				}
				$variableName=$tokens[$i-$j][1];
				$variableName=substr($variableName,1); //remove the $ sign
				//check if this name already used on another widget
				if (isset(self::$widgetInfo[$variableName]))
					throw new Exception("Name used for widget object at {$file}:{$line} already used for another widget at ".
						self::$widgetInfo[$variableName]['file'].":".self::$widgetInfo[$variableName]['line']);
				//store this new widget information
				self::$widgetInfo[$variableName]=array("class"=>$classname,"file"=>$file,"line"=>$line);
				return $variableName;
			}
		}
	}
	//this should not happen!
	throw new Exception("Could not find appropariate jWidget instanciation in {$file}:{$line} (Maybe you forgot to call parent::__construct(\$Parent) in your widget constructor?)");
}

First, I know that this code can be optimized much, that is not of the essence here. Lines 3 to 10, have the responsibility of finding the callee, i.e the file and the line where the class is instantiated. The file is used for tokenizer and the line for error messages. Lines 12 to 22 will be described later.

Next we tokenize the PHP file and start looping through the tokens until we find a T_NEW (new object) token. From there, we go forward until we find our desired class name (e.g "jForm"). Since many instantiations of the same class could be made in one file, we keep track of them (the counting of lines 12 to 22) so that for the n-th instance of the same class, we search for the n-th occurrence of "new thatClass()".

When we find that, we go back in tokens searching for an assignment mark. If not found, it is an improper usage (e.g something like $form1 = new jForm(); new jFormButton($form1);) so we trigger an error. Same goes if a variable is not before the assignment mark.

Finally we store the variable name and check if it is not used for any other widget (names are unique). If it is, the line and file of code where the other one is defined are outputted as an error.

The entire approach is pretty straightforward and simple, but it provides for an elegant design. This ends is actually achieved by a middle compiler (such as MOC in Qt) but due to very generic nature of PHP, meta-reflection is easily achieved by a run-time code parser. I will let you know when the library is out, both as a part of jframework and as a stand-alone library

Tags: , , , , , , , , , , ,

Trackback from your site.

Comments (1)

Leave a comment