parseExpression in Twig - php

I am integrating Twig in an existing project.
I am writing a token parser to parse a custom tag that is similiar to the {% render %} tag.
My tag looks like:
{% mytag 'somestring' with { 'name': name, 'color': 'green' } %}
where name is defined by {% set name = 'foo' %}
I am able to parse somestring without any issues.
This is the code used to parse the stuff in the with{ }:
$Stream = $this->parser->getStream();
if ( $Stream->test( \Twig_Token::NAME_TYPE, 'with' ) )
{
$Stream->next();
$parsedParameters = $this->parser->getExpressionParser()->parseExpression();
$parameters = $this->parser->getEnvironment()->compile( $parsedParameters );
var_dump( $parameters ); //string 'array( "name" => $this->getContext( $context, "name" ), "color" => "green" )' (length=72)
foreach ( $parsedParameters->getIterator() as $parameter )
{
//var_dump($ parameter->getAttribute('value') );
}
}
My goal is to turn 'name': name, 'color': 'green' into an associative array within the token parser:
array(
'name' => 'foo',
'color': 'green',
)
As the documentation is quite sparse and the code in the library is uncommented, I am not sure how to do this. If I loop through $parsedParameters, I get 4 elements consisting of the array key and an array value. However, as name is a variable with a type Twig_Node_Expression_Name, I am unsure as to how I can compile it to get the compiled value. Currently, I have found a way to compile that node, but all it gives me is a string containing a PHP expression which I can't use.
How can I turn the parsed expression into an associative array?

Okey. I guess I was able to solve that. Not to pretty, but should work.
$Stream = $this->parser->getStream();
if ( $Stream->test( \Twig_Token::NAME_TYPE, 'with' ) )
{
$Stream->next();
$parsedParameters = $this->parser->getExpressionParser()->parseExpression();
$parameters = $this->parser->getEnvironment()->compile( $parsedParameters );
var_dump( $parameters ); //string 'array( "name" => $this->getContext( $context, "name" ), "color" => "green" )' (length=72)
$index = null;
$value = null;
foreach ( $parsedParameters->getIterator() as $parameter )
{
if ( $parameter->hasAttribute( 'value' ) )
{
$index = $parameter->getAttribute( 'value' );
}
elseif ( $parameter->hasAttribute( 'name' ) )
{
$value = $parameter->getAttribute( 'name' );
}
if ( isset( $index, $value ) )
{
$params[ $index ] = $value;
$index = null;
$value = null;
}
}
}
So here I have array params, that I can pass to Custom node.
$params = var_export( $properties['params'], true );
unset( $fieldProperties['params'] );
Now I just did following:
$Compiler
->write( "\$params = array();\n" )
->write( "foreach ( {$params} as \$searchFor => \$replaceWith )\n" )
->write( "{\n" )
->write( "\t\$params[ \$searchFor ] = str_replace( \$replaceWith, \$context[\$replaceWith], \$replaceWith );\n" )
->write( "}\n" )
->write( "var_dump( \$params );\n" );
This should be it.
Also I see, that you where talking about TokenParser, but sadly I haven't found the solution to turn it over there.

Related

PHP parse_str() function allowing passing shorthand array

We are accepting strings from templates to a markup engine, which allows for configuration to be passed in a "simple" form.
The engine parses the strings via PHP, using an adapted version of the parse_str() function - so we can parse any combination of the strings below:
config=posts_per_page:"5",default:"No questions yet -- once created they will appear here."&markup->template="{{ questions }}"
gives:
Array(
[config] => Array
(
[posts_per_page] => 5
[default] => No questions yet -- once created they will appear here.
)
[markup] => Array
(
[template] => {{ questions }}
)
)
OR:
config->default=all:"<p class='ml-3'>No members here yet...</p>"
To Get:
Array
[config] => Array
(
[default] => Array
(
[all] => <p class='ml-3'>No members here yet...</p>
)
)
)
Another:
config=>handle:"medium"
Returns:
Array (
[config] => Array
(
[>handle] => medium
)
)
Strings can be passed with spaces ( and multi-line spaces ) and string parameters should be passed between "double quotes" to preserve natural spacing - we run the following preg_replace on the string before it is passed to the parse_str method:
// strip white spaces from data that is not passed inside double quotes ( "data" ) ##
$string = preg_replace( '~"[^"]*"(*SKIP)(*F)|\s+~', "", $string );
So far, so good - until we try to pass a "delimiter" inside a string value, then it is treated literally - for example the following string returns a corrupt array:
config=posts_per_page:"5",default:"No questions yet -- once created, they will appear here."&markup->template="{{ questions }}"
Returns the following array:
Array (
[config] => Array
(
[posts_per_page] => 5
[default] => No questions yet -- once created
[ they will appear here."] =>
)
[markup] => Array
(
[template] => {{ questions }}
)
)
The "," was treated literally and the string was broken into an extra array part.
One simple solution is to create delimiters and operators with a lower chance of conflicting with string values - for example changing "," to "###" - but one important part of the markup used is that it is easy to write and read - it's intended use-case is for front-end developers to pass simple arguments to the template parser - this is one reason we have tried to avoid JSON - which is of course a good fit in terms of passing data, but it's hard to read and write - of course, that statement is subjective and open to opinion :)
Here is the parse_str method:
public static function parse_str( $string = null ) {
// h::log($string);
// delimiters ##
$operator_assign = '=';
$operator_array = '->';
$delimiter_key = ':';
$delimiter_and_property = ',';
$delimiter_and_key = '&';
// check for "=" delimiter ##
if( false === strpos( $string, $operator_assign ) ){
h::log( 'e:>Passed string format does not include asssignment operator "'.$operator_assign.'" -- '.$string );
return false;
}
# result array
$array = [];
# split on outer delimiter
$pairs = explode( $delimiter_and_key, $string );
# loop through each pair
foreach ( $pairs as $i ) {
# split into name and value
list( $key, $value ) = explode( $operator_assign, $i, 2 );
// what about array values ##
// example -- sm:medium, lg:large
if( false !== strpos( $value, $delimiter_key ) ){
// temp array ##
$value_array = [];
// split value into an array at "," ##
$value_pairs = explode( $delimiter_and_property, $value );
// h::log( $value_pairs );
# loop through each pair
foreach ( $value_pairs as $v_pair ) {
// h::log( $v_pair ); // 'sm:medium'
# split into name and value
list( $value_key, $value_value ) = explode( $delimiter_key, $v_pair, 2 );
$value_array[ $value_key ] = $value_value;
}
// check if we have an array ##
if ( is_array( $value_array ) ){
$value = $value_array;
}
}
// $key might be in part__part format, so check ##
if( false !== strpos( $key, $operator_array ) ){
// explode, max 2 parts ##
$md_key = explode( $operator_array, $key, 2 );
# if name already exists
if( isset( $array[ $md_key[0] ][ $md_key[1] ] ) ) {
# stick multiple values into an array
if( is_array( $array[ $md_key[0] ][ $md_key[1] ] ) ) {
$array[ $md_key[0] ][ $md_key[1] ][] = $value;
} else {
$array[ $md_key[0] ][ $md_key[1] ] = array( $array[ $md_key[0] ][ $md_key[1] ], $value );
}
# otherwise, simply stick it in a scalar
} else {
$array[ $md_key[0] ][ $md_key[1] ] = $value;
}
} else {
# if name already exists
if( isset($array[$key]) ) {
# stick multiple values into an array
if( is_array($array[$key]) ) {
$array[$key][] = $value;
} else {
$array[$key] = array($array[$key], $value);
}
# otherwise, simply stick it in a scalar
} else {
$array[$key] = $value;
}
}
}
// h::log( $array );
# return result array
return $array;
}
I will try to skip splitting string between "double quotes" - probably via another regex, but perhaps there are other potential pitfalls waiting that might not make this approach viable long-term - any help glady accepted!
One solution, is to change the following:
from:
$value_pairs = explode( $delimiter_and_property, $value );
to:
$value_pairs = self::quoted_explode( $value, $delimiter_and_property, '"' );
which calls a new method found on another SO answer ( linked in comment block ):
/**
* Regex Escape values
*/
public static function regex_escape( $subject ) {
return str_replace( array( '\\', '^', '-', ']' ), array( '\\\\', '\\^', '\\-', '\\]' ), $subject );
}
/**
* Explode string, while respecting delimiters
*
* #link https://stackoverflow.com/questions/3264775/an-explode-function-that-ignores-characters-inside-quotes/13755505#13755505
*/
public static function quoted_explode( $subject, $delimiter = ',', $quotes = '\"' )
{
$clauses[] = '[^'.self::regex_escape( $delimiter.$quotes ).']';
foreach( str_split( $quotes) as $quote ) {
$quote = self::regex_escape( $quote );
$clauses[] = "[$quote][^$quote]*[$quote]";
}
$regex = '(?:'.implode('|', $clauses).')+';
preg_match_all( '/'.str_replace('/', '\\/', $regex).'/', $subject, $matches );
return $matches[0];
}

Turn string into multi-dimensional array equivalent

I'm trying to parse a string from a URL and convert it into a PHP array, but so far I haven't been able to get it to work correctly.
The string contains comma separated values which are then grouped by periods, for example:
course.id,course.title,course.author,course.duration,course.trailer.video_key,course.trailer.duration,course.lessons.id,course.lessons.title,track.id,track.title,track.caption.srt,track.caption.vtt,track.caption.text
The Array equivalent to this string would be:
PHP:
$array = [
'course' => [
'id',
'title',
'author',
'duration',
'trailer' => [
'video_key',
'duration'
],
'lessons' => [
'id',
'title'
]
],
'track' => [
'id',
'title',
'caption' => [
'srt',
'vtt',
'text'
]
]
];
My current solution is this:
PHP:
$string = 'course.id,course.title,course.author,course.duration,course.trailer.video_key,course.trailer.duration,course.lessons.id,course.lessons.title,track.id,track.title,track.caption.srt,track.caption.vtt,track.caption.text';
$parameters = explode( ',', $string );
$array = array();
$inset = array();
foreach ( $parameters as $parameter ) {
$segments = explode( '.', $parameter );
if ( ! array_key_exists( $segments[0], $inset ) ) {
$array[ $segments[0] ] = array();
$inset[ $segments[0] ] = true;
}
$array[ $segments[0] ] = array_merge( $array[ $segments[0] ], addSegment( $segments, 1 ) );
}
function addSegment( $segments, $counter ) {
$results = array();
if ( end( $segments ) == $segments[ $counter ] ) {
return array( $segments[ $counter ] => null );
}
$results[ $segments[ $counter ] ] = addSegment( $segments, $counter + 1 );
return $results;
}
This somewhat works, however; it fails with simpler string like this one
course,track,lessons
I think this calls for recursion but I'm not good at writing this so I'm hoping someone has already done this and can help me.
Thank you.
You can achieve this without recursion - just double loop and use of &:
$arr = explode(",", "course.id,course.title,course.author,course.duration,course.trailer.video_key,course.trailer.duration,course.lessons.id,course.lessons.title,track.id,track.title,track.caption.srt,track.caption.vtt,track.caption.text");
foreach($arr as $e) {
$e = explode(".", $e);
$obj = &$res; //set the current place root
while(count($e) > 1) { // last element will be add as value
$key = array_shift($e);
if (!isset($obj[$key]))
$obj[$key] = [];
$obj = &$obj[$key]; //reset the current place
}
$obj[] = array_shift($e); // add the last as value to current place
}
$res will contain your desire output

PHP: Find an array within an array, without looping

I have a simple array of boolean arrays as follows:
array(
array('dog' => true),
array('cat' => true),
array('bird' =>false),
array('fish' => true)
)
How can I find an entry, such as 'cat', without a loop construct? I think I should be able to accomplish this with a php array function, but the solution is eluding me! I just want to know if 'cat' is a valid key - I'm not interested in it's value.
In the example above, 'cat' should return true, while 'turtle' should return false.
$array = array(
array('dog' => true),
array('cat' => true),
array('bird' =>false),
array('fish' => true)
);
array_walk($array,'test_array');
function test_array($item2, $key){
$isarray = is_array($item2) ? 'Is Array<br>' : 'No array<br>';
echo $isarray;
}
using array_walk example in the manual
Output:
Is Array
Is Array
Is Array
Is Array
Ideone
You could achieve it like this :
Reduce your array to a single dimensional array using combination of array_reduce and array_merge PHP functions .
In the reduced array , look for the key using array_key_exists .
Your array :
$yourArray = array
(
array( 'dog' => true )
,array( 'cat' => true )
,array( 'bird' =>false )
,array( 'fish' => true )
);
Code to check if a key exists :
$itemToFind = 'cat'; // turtle
$result =
array_key_exists
(
$itemToFind
,array_reduce(
$yourArray
,function ( $v , $w ){ return array_merge( $v ,$w ); }
,array()
)
);
var_dump( $result );
Code to check if a key exits and to retrieve its value :
$itemToFind = 'cat'; // bird
$result =
array_key_exists
(
$itemToFind
,$reducedArray = array_reduce(
$yourArray
,function ( $v , $w ){ return array_merge( $v ,$w ); }
,array()
)
) ?$reducedArray[ $itemToFind ] :null;
var_dump( $result );
Using PHP > 5.5.0
You could use combination of array_column and count PHP functions to achieve it :
Code to check if a key exists :
$itemToFind = 'cat'; // turtle
$result = ( count ( array_column( $yourArray ,'cat' ) ) > 0 ) ? true : false;
var_dump( $result );
The above code tested with PHP 5.5.5 is here

Create shortcode with parameter in PHP Joomla

I've created a simple shortcode plugin on Joomla.
Actually I am trying to integrate Cleeng Video with Joomla. And will connect it's users in the future ( I hope ).
I've stack on creating shortcode's parameter. I don't know how to parse it's parameter and value.
My Shortcode is here (no parameter)
{cleengvideo}<iframe class="wistia_embed" src="http://fast.wistia.net/embed/iframe/5r8r9ib6di" name="wistia_embed" width="640" height="360" frameborder="0" scrolling="no" allowfullscreen=""></iframe>{/cleengvideo}
My code is here
public function onContentPrepare($content, $article, $params, $limit) {
preg_match_all('/{cleengvideo}(.*?){\/cleengvideo}/is', $article->text, $matches);
$i = 0;
foreach ($matches[0] as $match) {
$videoCode = $matches[1][$i];
$article->text = str_replace($match, $videoCode, $article->text);
}
I want to set height, width and 5r8r9ib6di this code from shortcode at least.
Please can anyone help me with adding and parsing it's parameter
To get a parameter, you can simply use the following code:
$params->get('param_name', 'default_value');
So for example, in your XML file, if you had a field like so:
<field name="width" type="text" label="Width" default="60px" />
you would call the parameter like so:
$params->get('width', '60px');
Note that you don't have to add the default value as the second string, however I always find it good practice.
Hope this helps
I think I could found it's solution.
It's here https://github.com/Cleeng/cleeng-wp-plugin/blob/master/php/classes/Frontend.php
Code is
$expr = '/\[cleeng_content(.*?[^\\\])\](.*?[^\\\])\[\/cleeng_content\]/is';
preg_match_all( $expr, $post->post_content, $m );
foreach ( $m[0] as $key => $content ) {
$paramLine = $m[1][$key];
$expr = '/(\w+)\s*=\s*(?:\"|")(.*?)(?<!\\\)(?:\"|")/si';
preg_match_all( $expr, $paramLine, $mm );
if ( ! isset( $mm[0] ) || ! count( $mm[0] ) ) {
continue;
}
$params = array( );
foreach ( $mm[1] as $key => $paramName ) {
$params[$paramName] = $mm[2][$key];
}
if ( ! isset( $params['id'] ) ) {
continue;
}
$content = array(
'contentId' => $params['id'],
'shortDescription' => #$params['description'],
'price' => #$params['price'],
'itemType' => 'article',
'purchased' => false,
'shortUrl' => '',
'referred' => false,
'referralProgramEnabled' => false,
'referralRate' => 0,
'rated' => false,
'publisherId' => '000000000',
'publisherName' => '',
'averageRating' => 4,
'canVote' => false,
'currencySymbol' => '',
'sync' => false
);
if ( isset( $params['referral'] ) ) {
$content['referralProgramEnabled'] = true;
$content['referralRate'] = $params['referral'];
}
if ( isset( $params['ls'] ) && isset( $params['le'] ) ) {
$content['hasLayerDates'] = true;
$content['layerStartDate'] = $params['ls'];
$content['layerEndDate'] = $params['le'];
}
$this->cleeng_content[$params['id']] = $content;
}
Hope this helps someone searching for shortcode parameters, for parameters in short code we can use preg_match_all like that
preg_match_all('/{cleengvideo(.*?)}(.*?){\/cleengvideo}/is', $article->text, $matches);
This will give a array with 3 array elements, the second array have the parameters which you can maupulate with codes.
Hope this helps.

Dynamic formation of variable name in PHP?

The code below dynamically concatenates keys to an existing array $options_pool. So the final form should be: $options_pool[ $base_key ][ $first_key ][ $second_key ]... This is so I can dynamically assign a value to the elements of the array $options_pool which is multidimensional.
foreach( $this->post_vars as $name => $value ) {
//Look for $name key in array $options_pool if it exists.
//Use multi_array_key_exists() to handle the task
//It should return something like "fruit:mango:apple_mango"
//Then dynamically call $options_pool based on the data. Like so: $options_pool[ 'fruit' ][ 'mango' ][ 'apple_mango' ] = $value;
$match_key = multi_array_key_exists( $name, $options_pool );
$option_keys = explode( ':', $match_key );
$option_keys_length = count( $option_keys );
$option_name_array = array();
if( 0 < $option_keys_length ) {
for( $c = $option_keys_length; $c > 0; $c-- ) {
$sub_keys = '$options_pool';
for( $c_sub = 0; $c_sub < $c ; $c_sub++ ) {
$sub_keys .= '[ $option_keys[ '. $c_sub . ' ] ]';
}
$option_name_array[] = $sub_keys;
}
foreach( $option_name_array as $f_var_name ) {
//the following line should equal to: $options_pool[ 'fruit' ][ 'mango' ][ 'apple_mango' ] = $value;
$f_var_name = $value;
}
}
}
//The $options_pool array
$options_pool = array( 'car' => '', 'animals' => '', 'fruit' => array( 'mango' => array( 'apple_mango' => '' ));
I think the logic is correct except that this portion of the code:
foreach( $option_name_array as $f_var_name ) {
$f_var_name = $value; //particularly this line
}
doesn't work? I've tested printing the value of $f_var_name and the result is correct but it doesn't really call the array?
That is incorrect, you are right.
The variable name will always be $options_pool.
You can pass the keys as explode(':', $name) and later assign them.
By the way, your code at
foreach( $option_keys as $option_keys_value ) {
$option_key_names[] = $option_keys_value;
}
Do you realize that it just copies $option_keys as $option_key_names just as if you had written $option_key_names = $option_keys; (in this code) ?
Maybe with a stack you could do this iteratively but with recursion it is just more natural, as you see here
function getVariableToWrite(&$reference, $nested_opts, $write) {
if(empty($nested_ops)) // Base case
return $reference = write;
if(isset($reference[array_shift($nested_ops)]))
return getVariableToWrite($reference, $nested_ops, $write);
throw new Exception("The key does not exist..");
}
And then just
foreach( $this->post_vars as $name => $value ) {
// Your work over here until...
$match_key = "fruit:mango:apple_mango";
$option_keys = explode( ':', $match_key );
getVariableToWrite($options_pool, $option_keys, $value);
}
Will this do the work?
In your foreach, you need to pass the value by reference, so you can edit it.
foreach( $option_name_array as &$f_var_name ){
$f_var_name = $value;
}
Try this...
foreach( $option_name_array as $key => $f_var_name ) {
$option_name_array[$key] = $value; //particularly this line
}

Categories