Skip to content

Commit db8557a

Browse files
committed
Added support foo.some_thing === foo.someThing for properties and array calls
1 parent e113cf4 commit db8557a

File tree

3 files changed

+192
-44
lines changed

3 files changed

+192
-44
lines changed

ext/twig/twig.c

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -735,10 +735,11 @@ static int twig_add_property_to_class(void *pDest APPLY_TSRMLS_DC, int num_args,
735735
zend_class_entry *ce;
736736
zval *retval;
737737
char *class_name, *prop_name;
738+
738739
zend_property_info *pptr = (zend_property_info *) pDest;
739740
APPLY_TSRMLS_FETCH();
740741

741-
if (!(pptr->flags & ZEND_ACC_PUBLIC) || (pptr->flags & ZEND_ACC_STATIC)) {
742+
if ((pptr->flags & ZEND_ACC_PRIVATE) || (pptr->flags & ZEND_ACC_STATIC)) {
742743
return 0;
743744
}
744745

@@ -752,6 +753,7 @@ static int twig_add_property_to_class(void *pDest APPLY_TSRMLS_DC, int num_args,
752753
#endif
753754

754755
add_assoc_string(retval, prop_name, prop_name, 1);
756+
add_assoc_string(retval, TWIG_DECAMELIZE(prop_name), prop_name, 1);
755757

756758
return 0;
757759
}
@@ -771,6 +773,7 @@ static void twig_add_class_to_cache(zval *cache, zval *object, char *class_name
771773
array_init(class_properties);
772774
// add all methods to self::cache[$class]['methods']
773775
zend_hash_apply_with_arguments(&class_ce->function_table APPLY_TSRMLS_CC, twig_add_method_to_class, 1, class_methods);
776+
// add all properties to self::cache[$class]['properties']
774777
zend_hash_apply_with_arguments(&class_ce->properties_info APPLY_TSRMLS_CC, twig_add_property_to_class, 2, &class_ce, class_properties);
775778

776779
add_assoc_zval(class_info, "methods", class_methods);
@@ -815,14 +818,46 @@ PHP_FUNCTION(twig_template_get_attributes)
815818
type = "any";
816819
}
817820

821+
/*
822+
if (is_object($object)) {
823+
$class = get_class($object);
824+
if (!isset(self::$cache[$class])) {
825+
self::$cache[$class] = $this->getCacheForClass($class);
826+
}
827+
}
828+
*/
829+
if (Z_TYPE_P(object) == IS_OBJECT) {
830+
class_name = TWIG_GET_CLASS_NAME(object TSRMLS_CC);
831+
tmp_self_cache = TWIG_GET_STATIC_PROPERTY(template, "cache" TSRMLS_CC);
832+
tmp_class = TWIG_GET_ARRAY_ELEMENT(tmp_self_cache, class_name, strlen(class_name) TSRMLS_CC);
833+
834+
if (!tmp_class) {
835+
twig_add_class_to_cache(tmp_self_cache, object, class_name TSRMLS_CC);
836+
tmp_class = TWIG_GET_ARRAY_ELEMENT(tmp_self_cache, class_name, strlen(class_name) TSRMLS_CC);
837+
}
838+
efree(class_name);
839+
}
840+
818841
/*
819842
// array
820843
if (Twig_Template::METHOD_CALL !== $type) {
821844
$arrayItem = is_bool($item) || is_float($item) ? (int) $item : $item;
845+
$hasArrayItem = false;
846+
847+
if (is_array($object) && array_key_exists($arrayItem, $object)) {
848+
$hasArrayItem = true;
849+
} elseif ($object instanceof ArrayAccess) {
850+
if (isset($object[$arrayItem])) {
851+
$hasArrayItem = true;
852+
} elseif (isset(self::$cache[$class]['properties'][$arrayItem])
853+
&& isset($object[self::$cache[$class]['properties'][$arrayItem]])
854+
) {
855+
$arrayItem = self::$cache[$class]['properties'][$arrayItem];
856+
$hasArrayItem = true;
857+
}
858+
}
822859
823-
if ((is_array($object) && array_key_exists($arrayItem, $object))
824-
|| ($object instanceof ArrayAccess && isset($object[$arrayItem]))
825-
) {
860+
if ($hasArrayItem) {
826861
if ($isDefinedTest) {
827862
return true;
828863
}
@@ -833,15 +868,30 @@ PHP_FUNCTION(twig_template_get_attributes)
833868

834869

835870
if (strcmp("method", type) != 0) {
836-
if ((TWIG_ARRAY_KEY_EXISTS(object, zitem))
837-
|| (TWIG_INSTANCE_OF(object, zend_ce_arrayaccess TSRMLS_CC) && TWIG_ISSET_ARRAYOBJECT_ELEMENT(object, zitem TSRMLS_CC))
838-
) {
871+
zval *tmp_properties, *tmp_item = NULL;
872+
873+
if (TWIG_ARRAY_KEY_EXISTS(object, zitem)) {
874+
tmp_item = zitem;
875+
} else if (TWIG_INSTANCE_OF(object, zend_ce_arrayaccess TSRMLS_CC)) {
876+
if (TWIG_ISSET_ARRAYOBJECT_ELEMENT(object, zitem TSRMLS_CC)) {
877+
tmp_item = zitem;
878+
} else {
879+
tmp_properties = TWIG_GET_ARRAY_ELEMENT(tmp_class, "properties", strlen("properties") TSRMLS_CC);
880+
if ((tmp_item = TWIG_GET_ARRAY_ELEMENT(tmp_properties, item, item_len TSRMLS_CC))
881+
&& !TWIG_ISSET_ARRAYOBJECT_ELEMENT(object, tmp_item TSRMLS_CC)
882+
) {
883+
tmp_item = NULL;
884+
}
885+
}
886+
}
887+
888+
if (tmp_item) {
839889

840890
if (isDefinedTest) {
841891
RETURN_TRUE;
842892
}
843893

844-
ret = TWIG_GET_ARRAY_ELEMENT_ZVAL(object, zitem TSRMLS_CC);
894+
ret = TWIG_GET_ARRAY_ELEMENT_ZVAL(object, tmp_item TSRMLS_CC);
845895

846896
if (!ret) {
847897
ret = &EG(uninitialized_zval);
@@ -932,24 +982,21 @@ PHP_FUNCTION(twig_template_get_attributes)
932982

933983
return;
934984
}
935-
/*
936-
$class = get_class($object);
937-
*/
938-
939-
class_name = TWIG_GET_CLASS_NAME(object TSRMLS_CC);
940-
tmp_self_cache = TWIG_GET_STATIC_PROPERTY(template, "cache" TSRMLS_CC);
941-
tmp_class = TWIG_GET_ARRAY_ELEMENT(tmp_self_cache, class_name, strlen(class_name) TSRMLS_CC);
942-
943-
if (!tmp_class) {
944-
twig_add_class_to_cache(tmp_self_cache, object, class_name TSRMLS_CC);
945-
tmp_class = TWIG_GET_ARRAY_ELEMENT(tmp_self_cache, class_name, strlen(class_name) TSRMLS_CC);
946-
}
947-
efree(class_name);
948985

949986
/*
950987
// object property
951988
if (Twig_Template::METHOD_CALL !== $type) {
989+
$property = null;
990+
952991
if (isset($object->$item) || array_key_exists((string) $item, $object)) {
992+
$property = $item;
993+
} elseif (isset(self::$cache[$class]['properties'][$item])
994+
&& isset($object->{self::$cache[$class]['properties'][$item]})
995+
) {
996+
$property = self::$cache[$class]['properties'][$item];
997+
}
998+
999+
if (null !== $property) {
9531000
if ($isDefinedTest) {
9541001
return true;
9551002
}
@@ -963,23 +1010,30 @@ PHP_FUNCTION(twig_template_get_attributes)
9631010
}
9641011
*/
9651012
if (strcmp("method", type) != 0) {
966-
zval *tmp_properties, *tmp_item;
1013+
zval *tmp_properties, *tmp_item = NULL;
9671014

9681015
tmp_properties = TWIG_GET_ARRAY_ELEMENT(tmp_class, "properties", strlen("properties") TSRMLS_CC);
969-
tmp_item = TWIG_GET_ARRAY_ELEMENT(tmp_properties, item, item_len TSRMLS_CC);
9701016

971-
if (tmp_item || TWIG_HAS_PROPERTY(object, zitem TSRMLS_CC) || TWIG_HAS_DYNAMIC_PROPERTY(object, item, item_len TSRMLS_CC)) {
1017+
if (TWIG_HAS_PROPERTY(object, zitem TSRMLS_CC) || TWIG_HAS_DYNAMIC_PROPERTY(object, item, item_len TSRMLS_CC)) {
1018+
tmp_item = zitem;
1019+
} else if (tmp_item = TWIG_GET_ARRAY_ELEMENT(tmp_properties, item, item_len TSRMLS_CC)) {
1020+
if (!TWIG_HAS_PROPERTY(object, tmp_item TSRMLS_CC) && !TWIG_HAS_DYNAMIC_PROPERTY(object, Z_STRVAL_P(tmp_item), strlen(tmp_item) TSRMLS_CC)) {
1021+
tmp_item = NULL;
1022+
}
1023+
}
1024+
1025+
if (tmp_item) {
9721026
if (isDefinedTest) {
9731027
RETURN_TRUE;
9741028
}
9751029
if (TWIG_CALL_SB(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "hasExtension", "sandbox" TSRMLS_CC)) {
976-
TWIG_CALL_ZZ(TWIG_CALL_S(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "getExtension", "sandbox" TSRMLS_CC), "checkPropertyAllowed", object, zitem TSRMLS_CC);
1030+
TWIG_CALL_ZZ(TWIG_CALL_S(TWIG_PROPERTY_CHAR(template, "env" TSRMLS_CC), "getExtension", "sandbox" TSRMLS_CC), "checkPropertyAllowed", object, tmp_item TSRMLS_CC);
9771031
}
9781032
if (EG(exception)) {
9791033
return;
9801034
}
9811035

982-
ret = TWIG_PROPERTY(object, zitem TSRMLS_CC);
1036+
ret = TWIG_PROPERTY(object, tmp_item TSRMLS_CC);
9831037
RETURN_ZVAL(ret, 1, 0);
9841038
}
9851039
}

lib/Twig/Template.php

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -348,13 +348,32 @@ final protected function getContext($context, $item, $ignoreStrictCheck = false)
348348
*/
349349
protected function getAttribute($object, $item, array $arguments = array(), $type = Twig_Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false)
350350
{
351+
if (is_object($object)) {
352+
$class = get_class($object);
353+
if (!isset(self::$cache[$class])) {
354+
self::$cache[$class] = $this->getCacheForClass($class);
355+
}
356+
}
357+
351358
// array
352359
if (Twig_Template::METHOD_CALL !== $type) {
353360
$arrayItem = is_bool($item) || is_float($item) ? (int) $item : $item;
361+
$hasArrayItem = false;
362+
363+
if (is_array($object) && array_key_exists($arrayItem, $object)) {
364+
$hasArrayItem = true;
365+
} elseif ($object instanceof ArrayAccess) {
366+
if (isset($object[$arrayItem])) {
367+
$hasArrayItem = true;
368+
} elseif (isset(self::$cache[$class]['properties'][$arrayItem])
369+
&& isset($object[self::$cache[$class]['properties'][$arrayItem]])
370+
) {
371+
$arrayItem = self::$cache[$class]['properties'][$arrayItem];
372+
$hasArrayItem = true;
373+
}
374+
}
354375

355-
if ((is_array($object) && array_key_exists($arrayItem, $object))
356-
|| ($object instanceof ArrayAccess && isset($object[$arrayItem]))
357-
) {
376+
if ($hasArrayItem) {
358377
if ($isDefinedTest) {
359378
return true;
360379
}
@@ -395,28 +414,32 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ
395414
throw new Twig_Error_Runtime(sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s")', $item, gettype($object), $object), -1, $this->getTemplateName());
396415
}
397416

398-
$class = get_class($object);
399-
400417
// object property
401418
if (Twig_Template::METHOD_CALL !== $type) {
419+
$property = null;
420+
402421
if (isset($object->$item) || array_key_exists((string) $item, $object)) {
422+
$property = $item;
423+
} elseif (isset(self::$cache[$class]['properties'][$item])
424+
&& isset($object->{self::$cache[$class]['properties'][$item]})
425+
) {
426+
$property = self::$cache[$class]['properties'][$item];
427+
}
428+
429+
if (null !== $property) {
403430
if ($isDefinedTest) {
404431
return true;
405432
}
406433

407434
if ($this->env->hasExtension('sandbox')) {
408-
$this->env->getExtension('sandbox')->checkPropertyAllowed($object, $item);
435+
$this->env->getExtension('sandbox')->checkPropertyAllowed($object, $property);
409436
}
410437

411-
return $object->$item;
438+
return $object->$property;
412439
}
413440
}
414441

415442
// object method
416-
if (!isset(self::$cache[$class])) {
417-
self::$cache[$class] = $this->getCacheForClass($class);
418-
}
419-
420443
$call = false;
421444
if (isset(self::$cache[$class]['methods'][$item])) {
422445
$method = self::$cache[$class]['methods'][$item];
@@ -476,17 +499,32 @@ protected function getAttribute($object, $item, array $arguments = array(), $typ
476499
*/
477500
protected function getCacheForClass($class)
478501
{
502+
$cache = array('methods' => array(), 'properties' => array());
503+
479504
$methods = get_class_methods($class);
505+
if (!empty($methods)) {
506+
$cache['methods'] = array_combine($methods, $methods);
507+
$keys = array_merge(preg_replace('/^(?:get|is)_?(.++)$/i', '\\1', $methods), $methods);
508+
$keys = array_merge(preg_replace('/((?<=[a-z]|\d)[A-Z]|(?<!^)[A-Z](?=[a-z]))/', '_\\1', $keys), $keys);
509+
$cache['methods'] += array_change_key_case(array_combine($keys, array_merge($methods, $methods, $methods, $methods)));
510+
}
480511

481-
if (empty($methods)) {
482-
return array('methods' => array());
512+
$properties = array_keys(get_class_vars($class));
513+
$implements = class_implements($class, false);
514+
if (isset($implements['ArrayAccess']) || (isset($cache['methods']['__isset']) && isset($cache['methods']['__get']))) {
515+
$reflection = new ReflectionClass($class);
516+
foreach ($reflection->getProperties(ReflectionProperty::IS_PROTECTED) as $property) {
517+
$properties[] = $property->getName();
518+
}
483519
}
484520

485-
$cache = array_combine($methods, $methods);
486-
$keys = array_merge(preg_replace('/^(?:get|is)_?(.++)$/i', '\\1', $methods), $methods);
487-
$keys = array_merge(preg_replace('/((?<=[a-z]|\d)[A-Z]|(?<!^)[A-Z](?=[a-z]))/', '_\\1', $keys), $keys);
521+
if (!empty($properties)) {
522+
$properties = array_combine($properties, $properties);
523+
$cache['properties'] = array_flip(preg_replace('/((?<=[a-z]|\d)[A-Z]|(?<!^)[A-Z](?=[a-z]))/', '_\\1', $properties));
524+
$cache['properties'] = array_change_key_case(array_diff_key($cache['properties'], $properties));
525+
}
488526

489-
return array('methods' => $cache + array_change_key_case(array_combine($keys, array_merge($methods, $methods, $methods, $methods))));
527+
return $cache;
490528
}
491529

492530
/**

test/Twig/Tests/TemplateTest.php

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ public function testGetAttributeCallExceptions($useExt = false)
245245

246246
$object = new Twig_TemplateMagicMethodExceptionObject();
247247

248-
$this->assertEquals(null, $template->getAttribute($object, 'foo'));
248+
$this->assertNull($template->getAttribute($object, 'foo'));
249249
}
250250

251251
public function getGetAttributeTests()
@@ -367,6 +367,22 @@ public function getGetAttributeTests()
367367
array(true, 'httpresponsecode', $methodAndPropObject, 'get_http_response_code', array(), $methodType),
368368

369369
array(true, 'http2_response', $methodAndPropObject, 'http2_response', array(), $anyType),
370+
371+
array(true, 'responseCode', $methodAndPropObject, 'responseCode', array(), $anyType),
372+
array(true, 'responseCode', $methodAndPropObject, 'response_code', array(), $anyType),
373+
));
374+
375+
$magicPropertyObject = new Twig_TemplateMagicPropertyPropertyObject;
376+
$arrayAccessPropertyObject = new Twig_TemplatePropertyArrayAccess;
377+
378+
// additional property tests
379+
$tests = array_merge($tests, array(
380+
array(true, 'camelCase', $magicPropertyObject, 'camelCase', array(), $anyType),
381+
array(true, 'camelCase', $magicPropertyObject, 'camel_case', array(), $anyType),
382+
array(true, 'camelCase', $arrayAccessPropertyObject, 'camelCase', array(), $anyType),
383+
array(true, 'camelCase', $arrayAccessPropertyObject, 'camelCase', array(), $arrayType),
384+
array(true, 'camelCase', $arrayAccessPropertyObject, 'camel_case', array(), $anyType),
385+
array(true, 'camelCase', $arrayAccessPropertyObject, 'camel_case', array(), $arrayType),
370386
));
371387

372388
// tests when input is not an array or object
@@ -507,6 +523,21 @@ public function __get($name)
507523
}
508524
}
509525

526+
class Twig_TemplateMagicPropertyPropertyObject
527+
{
528+
protected $camelCase = 'camelCase';
529+
530+
public function __isset($name)
531+
{
532+
return isset($this->$name);
533+
}
534+
535+
public function __get($name)
536+
{
537+
return $this->$name;
538+
}
539+
}
540+
510541
class Twig_TemplateMagicPropertyObjectWithException
511542
{
512543
public function __isset($key)
@@ -556,6 +587,29 @@ public function offsetUnset($offset)
556587
}
557588
}
558589

590+
class Twig_TemplatePropertyArrayAccess implements ArrayAccess
591+
{
592+
protected $camelCase = 'camelCase';
593+
594+
public function offsetExists($offset)
595+
{
596+
return isset($this->$offset);
597+
}
598+
599+
public function offsetGet($offset)
600+
{
601+
return $this->$offset;
602+
}
603+
604+
public function offsetSet($offset, $value)
605+
{
606+
}
607+
608+
public function offsetUnset($offset)
609+
{
610+
}
611+
}
612+
559613
class Twig_TemplateMethodObject
560614
{
561615
public function getDefined()
@@ -634,6 +688,8 @@ public function get_http2_response()
634688
{
635689
return 'http2_response';
636690
}
691+
692+
public $responseCode = 'responseCode';
637693
}
638694

639695
class Twig_TemplateMagicMethodObject

0 commit comments

Comments
 (0)