root/trunk/framework/TComponent.php

Revision 2482, 25.4 kB (checked in by knut, 4 months ago)

updated copyright

  • Property svn:keywords set to Id
Line 
1<?php
2/**
3 * TComponent, TPropertyValue classes
4 *
5 * @author Qiang Xue <qiang.xue@gmail.com>
6 * @link http://www.pradosoft.com/
7 * @copyright Copyright &copy; 2005-2008 PradoSoft
8 * @license http://www.pradosoft.com/license/
9 * @version $Id$
10 * @package System
11 */
12
13/**
14 * TComponent class
15 *
16 * TComponent is the base class for all PRADO components.
17 * TComponent implements the protocol of defining, using properties and events.
18 *
19 * A property is defined by a getter method, and/or a setter method.
20 * Properties can be accessed in the way like accessing normal object members.
21 * Reading or writing a property will cause the invocation of the corresponding
22 * getter or setter method, e.g.,
23 * <code>
24 * $a=$this->Text;     // equivalent to $a=$this->getText();
25 * $this->Text='abc';  // equivalent to $this->setText('abc');
26 * </code>
27 * The signatures of getter and setter methods are as follows,
28 * <code>
29 * // getter, defines a readable property 'Text'
30 * function getText() { ... }
31 * // setter, defines a writable property 'Text', with $value being the value to be set to the property
32 * function setText($value) { ... }
33 * </code>
34 * Property names are case-insensitive. It is recommended that they are written
35 * in the format of concatenated words, with the first letter of each word
36 * capitalized (e.g. DisplayMode, ItemStyle).
37 *
38 * An event is defined by the presence of a method whose name starts with 'on'.
39 * The event name is the method name and is thus case-insensitive.
40 * An event can be attached with one or several methods (called event handlers).
41 * An event can be raised by calling {@link raiseEvent} method, upon which
42 * the attached event handlers will be invoked automatically in the order they
43 * are attached to the event. Event handlers must have the following signature,
44 * <code>
45 * function eventHandlerFuncName($sender,$param) { ... }
46 * </code>
47 * where $sender refers to the object who is responsible for the raising of the event,
48 * and $param refers to a structure that may contain event-specific information.
49 * To raise an event (assuming named as 'Click') of a component, use
50 * <code>
51 * $component->raiseEvent('OnClick');
52 * </code>
53 * To attach an event handler to an event, use one of the following ways,
54 * <code>
55 * $component->OnClick=$callback;  // or $component->OnClick->add($callback);
56 * $$component->attachEventHandler('OnClick',$callback);
57 * </code>
58 * The first two ways make use of the fact that $component->OnClick refers to
59 * the event handler list {@link TList} for the 'OnClick' event.
60 * The variable $callback contains the definition of the event handler that can
61 * be either a string referring to a global function name, or an array whose
62 * first element refers to an object and second element a method name/path that
63 * is reachable by the object, e.g.
64 * - 'buttonClicked' : buttonClicked($sender,$param);
65 * - array($object,'buttonClicked') : $object->buttonClicked($sender,$param);
66 * - array($object,'MainContent.SubmitButton.buttonClicked') :
67 *   $object->MainContent->SubmitButton->buttonClicked($sender,$param);
68 *
69 * @author Qiang Xue <qiang.xue@gmail.com>
70 * @version $Id$
71 * @package System
72 * @since 3.0
73 */
74class TComponent
75{
76    /**
77     * @var array event handler lists
78     */
79    private $_e=array();
80
81    /**
82     * Returns a property value or an event handler list by property or event name.
83     * Do not call this method. This is a PHP magic method that we override
84     * to allow using the following syntax to read a property:
85     * <code>
86     * $value=$component->PropertyName;
87     * </code>
88     * and to obtain the event handler list for an event,
89     * <code>
90     * $eventHandlerList=$component->EventName;
91     * </code>
92     * @param string the property name or the event name
93     * @return mixed the property value or the event handler list
94     * @throws TInvalidOperationException if the property/event is not defined.
95     */
96    public function __get($name)
97    {
98        $getter='get'.$name;
99        if(method_exists($this,$getter))
100        {
101            // getting a property
102            return $this->$getter();
103        }
104        else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name))
105        {
106            // getting an event (handler list)
107            $name=strtolower($name);
108            if(!isset($this->_e[$name]))
109                $this->_e[$name]=new TList;
110            return $this->_e[$name];
111        }
112        else
113        {
114            throw new TInvalidOperationException('component_property_undefined',get_class($this),$name);
115        }
116    }
117
118    /**
119     * Sets value of a component property.
120     * Do not call this method. This is a PHP magic method that we override
121     * to allow using the following syntax to set a property or attach an event handler.
122     * <code>
123     * $this->PropertyName=$value;
124     * $this->EventName=$handler;
125     * </code>
126     * @param string the property name or event name
127     * @param mixed the property value or event handler
128     * @throws TInvalidOperationException If the property is not defined or read-only.
129     */
130    public function __set($name,$value)
131    {
132        $setter='set'.$name;
133        if(method_exists($this,$setter))
134        {
135            $this->$setter($value);
136        }
137        else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name))
138        {
139            $this->attachEventHandler($name,$value);
140        }
141        else if(method_exists($this,'get'.$name))
142        {
143            throw new TInvalidOperationException('component_property_readonly',get_class($this),$name);
144        }
145        else
146        {
147            throw new TInvalidOperationException('component_property_undefined',get_class($this),$name);
148        }
149    }
150
151    /**
152     * Determines whether a property is defined.
153     * A property is defined if there is a getter or setter method
154     * defined in the class. Note, property names are case-insensitive.
155     * @param string the property name
156     * @return boolean whether the property is defined
157     */
158    public function hasProperty($name)
159    {
160        return method_exists($this,'get'.$name) || method_exists($this,'set'.$name);
161    }
162
163    /**
164     * Determines whether a property can be read.
165     * A property can be read if the class has a getter method
166     * for the property name. Note, property name is case-insensitive.
167     * @param string the property name
168     * @return boolean whether the property can be read
169     */
170    public function canGetProperty($name)
171    {
172        return method_exists($this,'get'.$name);
173    }
174
175    /**
176     * Determines whether a property can be set.
177     * A property can be written if the class has a setter method
178     * for the property name. Note, property name is case-insensitive.
179     * @param string the property name
180     * @return boolean whether the property can be written
181     */
182    public function canSetProperty($name)
183    {
184        return method_exists($this,'set'.$name);
185    }
186
187    /**
188     * Evaluates a property path.
189     * A property path is a sequence of property names concatenated by '.' character.
190     * For example, 'Parent.Page' refers to the 'Page' property of the component's
191     * 'Parent' property value (which should be a component also).
192     * @param string property path
193     * @return mixed the property path value
194     */
195    public function getSubProperty($path)
196    {
197        $object=$this;
198        foreach(explode('.',$path) as $property)
199            $object=$object->$property;
200        return $object;
201    }
202
203    /**
204     * Sets a value to a property path.
205     * A property path is a sequence of property names concatenated by '.' character.
206     * For example, 'Parent.Page' refers to the 'Page' property of the component's
207     * 'Parent' property value (which should be a component also).
208     * @param string property path
209     * @param mixed the property path value
210     */
211    public function setSubProperty($path,$value)
212    {
213        $object=$this;
214        if(($pos=strrpos($path,'.'))===false)
215            $property=$path;
216        else
217        {
218            $object=$this->getSubProperty(substr($path,0,$pos));
219            $property=substr($path,$pos+1);
220        }
221        $object->$property=$value;
222    }
223
224    /**
225     * Determines whether an event is defined.
226     * An event is defined if the class has a method whose name is the event name prefixed with 'on'.
227     * Note, event name is case-insensitive.
228     * @param string the event name
229     * @return boolean
230     */
231    public function hasEvent($name)
232    {
233        return strncasecmp($name,'on',2)===0 && method_exists($this,$name);
234    }
235
236    /**
237     * @return boolean whether an event has been attached one or several handlers
238     */
239    public function hasEventHandler($name)
240    {
241        $name=strtolower($name);
242        return isset($this->_e[$name]) && $this->_e[$name]->getCount()>0;
243    }
244
245    /**
246     * Returns the list of attached event handlers for an event.
247     * @return TList list of attached event handlers for an event
248     * @throws TInvalidOperationException if the event is not defined
249     */
250    public function getEventHandlers($name)
251    {
252        if(strncasecmp($name,'on',2)===0 && method_exists($this,$name))
253        {
254            $name=strtolower($name);
255            if(!isset($this->_e[$name]))
256                $this->_e[$name]=new TList;
257            return $this->_e[$name];
258        }
259        else
260            throw new TInvalidOperationException('component_event_undefined',get_class($this),$name);
261    }
262
263    /**
264     * Attaches an event handler to an event.
265     *
266     * The handler must be a valid PHP callback, i.e., a string referring to
267     * a global function name, or an array containing two elements with
268     * the first element being an object and the second element a method name
269     * of the object. In Prado, you can also use method path to refer to
270     * an event handler. For example, array($object,'Parent.buttonClicked')
271     * uses a method path that refers to the method $object->Parent->buttonClicked(...).
272     *
273     * The event handler must be of the following signature,
274     * <code>
275     * function handlerName($sender,$param) {}
276     * </code>
277     * where $sender represents the object that raises the event,
278     * and $param is the event parameter.
279     *
280     * This is a convenient method to add an event handler.
281     * It is equivalent to {@link getEventHandlers}($name)->add($handler).
282     * For complete management of event handlers, use {@link getEventHandlers}
283     * to get the event handler list first, and then do various
284     * {@link TList} operations to append, insert or remove
285     * event handlers. You may also do these operations like
286     * getting and setting properties, e.g.,
287     * <code>
288     * $component->OnClick[]=array($object,'buttonClicked');
289     * $component->OnClick->insertAt(0,array($object,'buttonClicked'));
290     * </code>
291     * which are equivalent to the following
292     * <code>
293     * $component->getEventHandlers('OnClick')->add(array($object,'buttonClicked'));
294     * $component->getEventHandlers('OnClick')->insertAt(0,array($object,'buttonClicked'));
295     * </code>
296     *
297     * @param string the event name
298     * @param callback the event handler
299     * @throws TInvalidOperationException if the event does not exist
300     */
301    public function attachEventHandler($name,$handler)
302    {
303        $this->getEventHandlers($name)->add($handler);
304    }
305
306    /**
307     * Detaches an existing event handler.
308     * This method is the opposite of {@link attachEventHandler}.
309     * @param string event name
310     * @param callback the event handler to be removed
311     * @return boolean if the removal is successful
312     */
313    public function detachEventHandler($name,$handler)
314    {
315        if($this->hasEventHandler($name))
316        {
317            try
318            {
319                $this->getEventHandlers($name)->remove($handler);
320                return true;
321            }
322            catch(Exception $e)
323            {
324            }
325        }
326        return false;
327    }
328
329    /**
330     * Raises an event.
331     * This method represents the happening of an event and will
332     * invoke all attached event handlers for the event.
333     * @param string the event name
334     * @param mixed the event sender object
335     * @param TEventParameter the event parameter
336     * @throws TInvalidOperationException if the event is undefined
337     * @throws TInvalidDataValueException If an event handler is invalid
338     */
339    public function raiseEvent($name,$sender,$param)
340    {
341        $name=strtolower($name);
342        if(isset($this->_e[$name]))
343        {
344            foreach($this->_e[$name] as $handler)
345            {
346                if(is_string($handler))
347                {
348                    if(($pos=strrpos($handler,'.'))!==false)
349                    {
350                        $object=$this->getSubProperty(substr($handler,0,$pos));
351                        $method=substr($handler,$pos+1);
352                        if(method_exists($object,$method))
353                            $object->$method($sender,$param);
354                        else
355                            throw new TInvalidDataValueException('component_eventhandler_invalid',get_class($this),$name,$handler);
356                    }
357                    else
358                        call_user_func($handler,$sender,$param);
359                }
360                else if(is_callable($handler,true))
361                {
362                    // an array: 0 - object, 1 - method name/path
363                    list($object,$method)=$handler;
364                    if(is_string($object))    // static method call
365                        call_user_func($handler,$sender,$param);
366                    else
367                    {
368                        if(($pos=strrpos($method,'.'))!==false)
369                        {
370                            $object=$this->getSubProperty(substr($method,0,$pos));
371                            $method=substr($method,$pos+1);
372                        }
373                        if(method_exists($object,$method))
374                            $object->$method($sender,$param);
375                        else
376                            throw new TInvalidDataValueException('component_eventhandler_invalid',get_class($this),$name,$handler[1]);
377                    }
378                }
379                else
380                    throw new TInvalidDataValueException('component_eventhandler_invalid',get_class($this),$name,gettype($handler));
381            }
382        }
383        else if(!$this->hasEvent($name))
384            throw new TInvalidOperationException('component_event_undefined',get_class($this),$name);
385    }
386
387    /**
388     * Evaluates a PHP expression in the context of this control.
389     * @return mixed the expression result
390     * @throws TInvalidOperationException if the expression is invalid
391     */
392    public function evaluateExpression($expression)
393    {
394        try
395        {
396            if(eval("\$result=$expression;")===false)
397                throw new Exception('');
398            return $result;
399        }
400        catch(Exception $e)
401        {
402            throw new TInvalidOperationException('component_expression_invalid',get_class($this),$expression,$e->getMessage());
403        }
404    }
405
406    /**
407     * Evaluates a list of PHP statements.
408     * @param string PHP statements
409     * @return string content echoed or printed by the PHP statements
410     * @throws TInvalidOperationException if the statements are invalid
411     */
412    public function evaluateStatements($statements)
413    {
414        try
415        {
416            ob_start();
417            if(eval($statements)===false)
418                throw new Exception('');
419            $content=ob_get_contents();
420            ob_end_clean();
421            return $content;
422        }
423        catch(Exception $e)
424        {
425            throw new TInvalidOperationException('component_statements_invalid',get_class($this),$statements,$e->getMessage());
426        }
427    }
428
429    /**
430     * This method is invoked after the component is instantiated by a template.
431     * When this method is invoked, the component's properties have been initialized.
432     * The default implementation of this method will invoke
433     * the potential parent component's {@link addParsedObject}.
434     * This method can be overridden.
435     * @param TComponent potential parent of this control
436     * @see addParsedObject
437     */
438    public function createdOnTemplate($parent)
439    {
440        $parent->addParsedObject($this);
441    }
442
443    /**
444     * Processes an object that is created during parsing template.
445     * The object can be either a component or a static text string.
446     * This method can be overridden to customize the handling of newly created objects in template.
447     * Only framework developers and control developers should use this method.
448     * @param string|TComponent text string or component parsed and instantiated in template
449     * @see createdOnTemplate
450     */
451    public function addParsedObject($object)
452    {
453    }
454}
455
456/**
457 * TEnumerable class.
458 * TEnumerable is the base class for all enumerable types.
459 * To define an enumerable type, extend TEnumberable and define string constants.
460 * Each constant represents an enumerable value.
461 * The constant name must be the same as the constant value.
462 * For example,
463 * <code>
464 * class TTextAlign extends TEnumerable
465 * {
466 *     const Left='Left';
467 *     const Right='Right';
468 * }
469 * </code>
470 * Then, one can use the enumerable values such as TTextAlign::Left and
471 * TTextAlign::Right.
472 *
473 * @author Qiang Xue <qiang.xue@gmail.com>
474 * @version $Id$
475 * @package System
476 * @since 3.0
477 */
478class TEnumerable
479{
480}
481
482/**
483 * TPropertyValue class
484 *
485 * TPropertyValue is a utility class that provides static methods
486 * to convert component property values to specific types.
487 *
488 * TPropertyValue is commonly used in component setter methods to ensure
489 * the new property value is of specific type.
490 * For example, a boolean-typed property setter method would be as follows,
491 * <code>
492 * function setPropertyName($value) {
493 *     $value=TPropertyValue::ensureBoolean($value);
494 *     // $value is now of boolean type
495 * }
496 * </code>
497 *
498 * Properties can be of the following types with specific type conversion rules:
499 * - string: a boolean value will be converted to 'true' or 'false'.
500 * - boolean: string 'true' (case-insensitive) will be converted to true,
501 *            string 'false' (case-insensitive) will be converted to false.
502 * - integer
503 * - float
504 * - array: string starting with '(' and ending with ')' will be considered as
505 *          as an array expression and will be evaluated. Otherwise, an array
506 *          with the value to be ensured is returned.
507 * - object
508 * - enum: enumerable type, represented by an array of strings.
509 *
510 * @author Qiang Xue <qiang.xue@gmail.com>
511 * @version $Id$
512 * @package System
513 * @since 3.0
514 */
515class TPropertyValue
516{
517    /**
518     * Converts a value to boolean type.
519     * Note, string 'true' (case-insensitive) will be converted to true,
520     * string 'false' (case-insensitive) will be converted to false.
521     * If a string represents a non-zero number, it will be treated as true.
522     * @param mixed the value to be converted.
523     * @return boolean
524     */
525    public static function ensureBoolean($value)
526    {
527        if (is_string($value))
528            return strcasecmp($value,'true')==0 || $value!=0;
529        else
530            return (boolean)$value;
531    }
532
533    /**
534     * Converts a value to string type.
535     * Note, a boolean value will be converted to 'true' if it is true
536     * and 'false' if it is false.
537     * @param mixed the value to be converted.
538     * @return string
539     */
540    public static function ensureString($value)
541    {
542        if (is_bool($value))
543            return $value?'true':'false';
544        else
545            return (string)$value;
546    }
547
548    /**
549     * Converts a value to integer type.
550     * @param mixed the value to be converted.
551     * @return integer
552     */
553    public static function ensureInteger($value)
554    {
555        return (integer)$value;
556    }
557
558    /**
559     * Converts a value to float type.
560     * @param mixed the value to be converted.
561     * @return float
562     */
563    public static function ensureFloat($value)
564    {
565        return (float)$value;
566    }
567
568    /**
569     * Converts a value to array type. If the value is a string and it is
570     * in the form (a,b,c) then an array consisting of each of the elements
571     * will be returned. If the value is a string and it is not in this form
572     * then an array consisting of just the string will be returned. If the value
573     * is not a string then
574     * @param mixed the value to be converted.
575     * @return array
576     */
577    public static function ensureArray($value)
578    {
579        if(is_string($value))
580        {
581            $value = trim($value);
582            $len = strlen($value);
583            if ($len >= 2 && $value[0] == '(' && $value[$len-1] == ')')
584            {
585                eval('$array=array'.$value.';');
586                return $array;
587            }
588            else
589                return $len>0?array($value):array();
590        }
591        else
592            return (array)$value;
593    }
594
595    /**
596     * Converts a value to object type.
597     * @param mixed the value to be converted.
598     * @return object
599     */
600    public static function ensureObject($value)
601    {
602        return (object)$value;
603    }
604
605    /**
606     * Converts a value to enum type.
607     *
608     * This method checks if the value is of the specified enumerable type.
609     * A value is a valid enumerable value if it is equal to the name of a constant
610     * in the specified enumerable type (class).
611     * For more details about enumerable, see {@link TEnumerable}.
612     *
613     * For backward compatibility, this method also supports sanity
614     * check of a string value to see if it is among the given list of strings.
615     * @param mixed the value to be converted.
616     * @param mixed class name of the enumerable type, or array of valid enumeration values. If this is not an array,
617     * the method considers its parameters are of variable length, and the second till the last parameters are enumeration values.
618     * @return string the valid enumeration value
619     * @throws TInvalidDataValueException if the original value is not in the string array.
620     */
621    public static function ensureEnum($value,$enums)
622    {
623        static $types=array();
624        if(func_num_args()===2 && is_string($enums))
625        {
626            if(!isset($types[$enums]))
627                $types[$enums]=new ReflectionClass($enums);
628            if($types[$enums]->hasConstant($value))
629                return $value;
630            else
631                throw new TInvalidDataValueException(
632                    'propertyvalue_enumvalue_invalid',$value,
633                        implode(' | ',$types[$enums]->getConstants()));
634        }
635        else if(!is_array($enums))
636        {
637            $enums=func_get_args();
638            array_shift($enums);
639        }
640        if(in_array($value,$enums,true))
641            return $value;
642        else
643            throw new TInvalidDataValueException('propertyvalue_enumvalue_invalid',$value,implode(' | ',$enums));
644    }
645}
646
647/**
648 * TEventParameter class.
649 * TEventParameter is the base class for all event parameter classes.
650 *
651 * @author Qiang Xue <qiang.xue@gmail.com>
652 * @version $Id$
653 * @package System
654 * @since 3.0
655 */
656class TEventParameter extends TComponent
657{
658}
659
660/**
661 * TComponentReflection class.
662 *
663 * TComponentReflection provides functionalities to inspect the public/protected
664 * properties, events and methods defined in a class.
665 *
666 * The following code displays the properties and events defined in {@link TDataGrid},
667 * <code>
668 *   $reflection=new TComponentReflection('TDataGrid');
669 *   Prado::varDump($reflection->getProperties());
670 *   Prado::varDump($reflection->getEvents());
671 * </code>
672 *
673 * @author Qiang Xue <qiang.xue@gmail.com>
674 * @version $Id$
675 * @package System
676 * @since 3.0
677 */
678class TComponentReflection extends TComponent
679{
680    private $_className;
681    private $_properties=array();
682    private $_events=array();
683    private $_methods=array();
684
685    /**
686     * Constructor.
687     * @param object|string the component instance or the class name
688     * @throws TInvalidDataTypeException if the object is not a component
689     */
690