How to extract start line of a property declaration in PHP? - php

With reflection, it's easy to get the start and end line e.g. of a method in the source file: ReflectionFunctionAbstract::getFileName(), ReflectionFunctionAbstract::getStartLine(), ReflectionFunctionAbstract::getEndLine() provide this functionality. However, this doesn't seem to work with properties. What's the best way to extract at least the start line and the file name of a property declaration in a class definition?

It's not trivial but also not too hard.
You can get the class a property is defined in via Reflection. And from there you can get the filename. All you have to do then is either tokenize the file and check at what line the property declaration or simply go over the file line by line and do string matching.
Here is one possible way to do that:
$reflector = new ReflectionProperty('Foo', 'bar');
$declaringClass = $reflector->getDeclaringClass();
$classFile = new SplFileObject($declaringClass->getFileName());
foreach ($classFile as $line => $content) {
if (preg_match(
'/
(private|protected|public|var) # match visibility or var
\s # followed 1 whitespace
\$bar # followed by the var name $bar
/x',
$content)
) {
echo $line + 1;
}
}
And here is a demo to show that it works
Obviously, the above solution assumes the property to be declared in a certain fashion. It also assumes you have one class per file. If you cannot be sure this is the case, tokenization is the better option. But it's also more difficult.

Use roave/better-reflection
$classInfo = (new BetterReflection())
->reflector()
->reflectClass($class);
foreach ( $classInfo->getProperties() as $reflectionProperty) {
$declaringClass = $reflectionProperty->getDeclaringClass()->getFileName();
$declaringSource = $reflectionProperty->getDeclaringClass()->getLocatedSource()->getSource();
$sourceLines = explode("\n", $declaringSource);
$propertySource = join("\n", array_slice($sourceLines, $reflectionProperty->getStartLine(), $reflectionProperty->getEndLine()-$reflectionProperty->getStartLine()));
$properties[$reflectionProperty->getName()] = [
'declaringClass' => $declaringClass,
'source' => $propertySource,
'startLine' => $reflectionProperty->getStartLine(),
'endLine' => $reflectionProperty->getEndLine()
];
}
print_r($properties);
The snippet above will also get the property declaration when that property is declared in a trait or parent class. Obviously, this can be optimized, as it's splitting the source inside the loop.

Related

preg_match_all multine code scanning matches with a first word match

first time posting a question here. I am currently writing a tool for reverse engineer PHP7 code in order to create UML class diagrams and for that purpose I'm using preg_match_all to extract sections of code from the source files. So far so good but I must admit I don't fully understand yet how regular expressions work. Still I was able to create complex patterns but this one beats me.
what I want is to match the use clause from classes body in order to get the traits names. It can be in one of these following formats *(as mentioned in https://www.php.net/manual/en/language.oop5.traits.php):
1)
use trait;
use othertrait;
2)
use trait, othertrait, someothertrait;
or
use trait, othertrait, someothertrait { "conflict_resolutions" }
I don't care about conflict resolutions yet so I can drop these.
so far I have the following regex pattern:
class usetrait_finder {
use finder;
function __construct( string $source ){
$this->source = $source;
$this->pattern = "/";
$this->pattern .= "(?:(use\s+|,)\s*(?<traitname>[a-zA-Z0-9_]*))";
$this->pattern .= "";
$this->pattern .= "/ms";
$this->matches($source);
}
function get_trait_name(): string {
return $this->matches["traitname"][$this->current_key];
}
}
which matches the mentioned cases, but I know it's a cheat because the "use" word must appear at least one at first. I wrote a PHPUnit test to check every normal case and the following test doesn't pass:
// tag "use" must be preset at least once
function test_invalid_source_2(){
$source = "function sarasa(
sometrait
,
someothertrait )
{ anything }
function test() {}
}";
$finder = new usetrait_finder( $source );
var_dump( $finder->matches($source)[0] );
$this->assertEquals( false, $finder->more_elements() );
}
the var_dump output is:
array(1) {
[0]=>
string(16) ",
someothertrait"
}
my expected output should be an empty string or null since the word "use" is not the first thing matched on the first line. Of course others tests must pass, and in the $matches["traitname"] should be only one trait name.
usetrait_finder is here:
https://github.com/rudymartinb/classtree/blob/master/src/usetrait_finder.php
"finder" trait section of above source is here (not really important but it won't hurt mentioning):
https://github.com/rudymartinb/classtree/blob/master/src/traits/finder.php
full test case is here: https://github.com/rudymartinb/classtree/blob/master/tests/usetrait_finder_Test.php
thank you in advance

How to replace a specific value in a hard-coded array inside of a php file?

I use a config.php file that returns an array. Before releasing the project, I usually manually change the 'apiKey' value that I use while developing in the file. I forget to perform this replacement sometimes, so I'm looking for a programmatical way to find this in a string version of the file:
'apiKey' => '1234567890'
and replace with this:
'apiKey' => 'YourAPIKeyHere'
The development apiKey value, spaces, tabs and formatting are inconsistent (Developer/IDE specific), so I guess there are wildcards for that?
Then I can just make the change in my deployment script.
Edit to show sample of config.php (which will be read into a string, edited, then re-written as a file).
<?php
return array(
// Comments with instruction exist throughout the file. They must remain.
'apiKey' => 'ecuhi3647325fdv23tjVncweuYtYTv532r3',
...
);
Edit: **There are instructional comments in the config.php file that must remain. So re-writing a modified array will lose the comments, and that is undesirable.
Save the config file's text in a variable called $content.
Then call:
$content = preg_replace("~'apiKey'\s*=>\s*'\K[^']+~", 'YourAPIKeyHere', $content, 1);
Then overwrite the file with the updated variable.
http://php.net/manual/en/function.preg-replace.php
\s* means match zero or more whitespace characters.
\K means restart the match from this point.
[^']+ means match one or more non-single-quote character.
Regex101 Demo
PHP Demo
I assume that you have a config file such as;
return [
'dbname' = 'project',
'username' = 'root',
'password' = '123456',
.
.
.
'apiKey' => '1234567890',
]
So you can make a small helper method then you can use it before relasing your project..
function reset_config()
{
$file_path = "your/config/path";
$configs = require_once($file_path);
array_walk_recursive($configs, function (&$config, $key) {
$config = "your " . $key;
});
$string = var_export($configs,true);
$new_config_file = <<<HEAD
<?php
return $string;
HEAD;
file_put_contents($file_path, $new_config_file);
}
so all you need to use reset_config() function before relasing the project
You can use this simple RegEx to match any line containing a key between the apostrophes:
'apiKey' => '[^']+'
The [^']+ will find one or more characters between single quotes.
Just replace with your new line.
Edit:
Your replacement string would simply be:
'apiKey' => 'EnterYourAPIKeyHere'
I solved the issue by reading the file into an array and replacing the line with 'apiKey':
$array = file('app/config.php');
$string = "";
for($i = 0, $maxi = count($array); $i < $maxi; $i++)
{
if(strpos($array[$i],'apiKey')>0){
$string.=" 'apiKey' => 'YourAppAPIKeyHere',\r\n\r\n";
}else{
$string.=$array[$i];
}
}
It may not be the most elegant solution but it works. Until someone doesn't format their code right. For this reason, I would still like to use a RegEx that isolates the replacement to the required pattern. But RegEx is something I just don't get into, and there's other issues to resolve now.
Inspired by everyone who helped.
Feedback appreciated.
As i suggested you can use the PHP tokenizer extension functions to achieve your purpose
function replaceApiKey($configpath,$newKey='test',$newpath=''){
if(file_exists($configpath)&&is_readable($configpath)&&is_file($configpath))
$string = file_get_contents($configpath);
else
return false;
$tokens=token_get_all($string);
$start=false;
foreach($tokens as $key=>$token){
if(is_array($token)&&stripos($token[1],'apiKey')){
$start=true;
$tokens[$key]=$token[1];
continue;
}
if($start&&$token&&is_array($token)&&token_name($token[0])!=="T_COMMENT"&&token_name($token[0])!=="T_DOUBLE_ARROW"&&!ctype_space($token[1])){
$token[1]=$token[1][0].$newKey.$token[1][strlen($token[1])-1];
$start=false;
}
if(is_array($token)) $tokens[$key]=$token[1];
}
if(empty($newpath))
$newpath=$configpath;
if (file_put_contents($newpath, join('',$tokens)))
return true;
else
return false;}
This function take in parameter the config path,tokenize the content then search and replace the old apiKey by the new one and save the changes in the new path...

How to emulate lookbehind within a regex with non-fixed width

I'm currently encountering a problem with Regex in given circumstances : I need to parse PHP source files (class especially) to look for constants that are defined within those files and to retrieve them back to the output.
Those constants can have some documentation (and that's why I left the idea of Reflection since retrieving constants via Reflection only returns their name and their value) that may be shipped within comments tags.
I did manage to build the two separate parts of the regex (1 being the comment tag, the other being the const declaration) but I can't manage to link them both successfully : it seems that the very first constant within the file will also contain all the previously declared elements until it reaches the very first comment block.
My regex is as follows (I'm not a regex God so feel free to bring any criticism) :
((\t\ )*(/\*+(.|\n)*\*/)\R+)?([\t| ]*(?|(public|protected|private)\s*)?const\s+([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\s*=\s*(.*);)
There goes the sample test : Regex101
In case the initial code disappears :
/**
*
*/
class Test {
/**
*
*/
public const LOL = "damn";
/**
*
*/
private const TEST = 5;
public const plop = "dong";
}
I did look there and there for tips and I've learnt about positive lookbehind but from what I understood, it only works with fixed-width patterns.
I'm running out of ideas.
I would favor a multi-step approach: separate every class, then look for comments (eventually) and for the constants. In terms of regex, this can be achieved via
class\h*(?P<classname>\w+)[^{}]* # look for class literally and capture the name
(\{
(?:[^{}]*|(?2))* # the whole block matches the class content
\})
See a demo on regex101.com.
Now, to the comments and constants
^\h*
(?:(?P<comment>\Q/*\E(?s:.*?)\Q*/\E)(?s:.*?))?
(?:public|private)\h*const\h*
(?P<key>\w+)\h*=\h*(?P<value>[^;]+)
See a demo for this step on regex101.com as well.
The last step would be to clean the comments:
^\h*/?\*+\h*/?
See a demo for the cleansing on regex101.com.
Lastly, you'll need two loops:
preg_match_all($regex_class, $source, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
preg_match_all($const_class, $match[0], $constants, PREG_SET_ORDER);
foreach ($constants as $constant) {
$comment = preg_replace($clean_comment, '', $constant["comment"]);
# find the actual values here
echo "Class: {$match["classname"]}, Constant Name: {$constant["key"]}, Constant Value: {$constant["value"]}, Comment: $comment\n";
}
}
An overall demo can be found on ideone.com.
Mind the individual regex modifiers in the demo and source code (especially verbose and multiline !).
You can do it in an array as well:
$result = [];
preg_match_all($regex_class, $source, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
preg_match_all($const_class, $match[0], $constants, PREG_SET_ORDER);
foreach ($constants as $constant) {
$comment = trim(preg_replace($clean_comment, '', $constant["comment"]));
$result[$match["classname"]][] = array('name' => $constant["key"], 'value' => $constant['value'], 'comment' => $comment);
}
}
print_r($result);
You can do it without positive lookbehind:
You have to match a comment, immediately followed by a const declaration:
(?:(?:^/\*\*$\s+)((?:^ ?\*.*$\s*?)+)(?:\s+^\*/$\s+))?^\s+(public|protected|private) const (\S+)\s+= ([^;]+);
The first group will allow you to retrieve the documentation:
Comment part
(?:^/\*\*$\s+) finds the beginning of a block comment
((?:^ ?\*.*$\s*?)+) represents the group containing the content of your comments
(?:\s+^\*/$\s+) end of the comment
Declaration part:
^\s+ to skip the whitespace at the beginning of the line
(public|protected|private) const a group to determine visibility
(\S+)\s+= ([^;]+); groups for the name and value

How to save regex backreferences to an array during preg_replace or preg_replace_callback

Here's the problem: I have a database full of articles marked up in XHTML. Our application uses Prince XML to generate PDFs. An artifact of that is that footnotes are marked up inline, using the following pattern:
<p>Some paragraph text<span class="fnt">This is the text of a footnote</span>.</p>
Prince replaces every span.fnt with a numeric footnote marker, and renders the enclosed text as a footnote at the bottom of the page.
We want to render the same content in ebook formats, and XHTML is a great starting point, but the inline footnotes are terrible. What I want to do is convert the footnotes to endnotes in my ebook build script.
This is what I'm thinking:
Create an empty array called $endnotes to store the endnote text.
Set a variable $endnote_no to zero. This variable will hold the current endnote number, to display inline as an endnote marker, and to be used in linking the endnote marker to the particular endnote.
Use preg_replace or preg_replace_callback to find every instance of <span class="fnt">(.*?)</span>.
Increment $endnote_no for each instance, and replace the inline span with '<sup><a href="#endnote_' . $endnote_no . '">' .$endnote_no . ''`
Push the footnote text to the $endnotes array so that I can use it at the end of the document.
After replacing all the footnotes with numeric endnote references, iterate through the $endnotes array to spit out the endnotes as an ordered list in XHTML.
This process is a bit beyond my PHP comprehension, and I get lost when I try to translate this into code. Here's what I have so far, which I mainly cobbled together based on code examples I found in the PHP documentation:
$endnotes = array();
$endnote_no = 0;
class Endnoter {
public function replace($subject) {
$this->endnote_no = 0;
return preg_replace_callback('`<span class="fnt">(.*?)</span>`', array($this, '_callback'), $subject);
}
public function _callback($matches) {
array_push($endnotes, $1);
return '<sup>' . $this->endnote_no . '</sup>';
}
}
...
$replacer = new Endnoter();
$replacer->replace($body);
echo '<pre>';
print_r($endnotes); // Just checking to see if the $endnotes are there.
echo '</pre>';
Any guidance would be helpful, especially if there is a simpler way to get there.
Don't know about a simpler way, but you were halfway there. This seems to work.
I just cleaned it up a bit, moved the variables inside your class and added an output method to get the footnote list.
class Endnoter
{
private $number_of_notes = 0;
private $footnote_texts = array();
public function replace($input) {
return preg_replace_callback('#<span class="fnt">(.*)</span>#i', array($this, 'replace_callback'), $input);
}
protected function replace_callback($matches) {
// the text sits in the matches array
// see http://php.net/manual/en/function.preg-replace-callback.php
$this->footnote_texts[] = $matches[1];
return '<sup>'.$this->number_of_notes.'</sup>';
}
public function getEndnotes() {
$out = array();
$out[] = '<ol>';
foreach($this->footnote_texts as $text) {
$out[] = '<li>'.$text.'</li>';
}
$out[] = '</ol>';
return implode("\n", $out);
}
}
First, you're best off not using a regex for HTML manipulation; see here:
How do you parse and process HTML/XML in PHP?
However, if you really want to go that route, there are a few things wrong with your code:
return '<sup>' . $this->endnote_no . '</sup>';
if endnote_no is 1, for example this will produce
'<sup>2</sup>';
If those values are both supposed to be the same, you want to increment endnote_no first:
return '<sup>' . $this->endnote_no . '</sup>';
Note the ++ in front of the call instead of after.
array_push($endnotes, $1);
$1 is not a defined value. You're looking for the array you passed in to the callback, so you want $matches[1]
print_r($endnotes);
$endnotes is not defined outside the class, so you either want a getter function to retrieve $endnotes (usually preferable) or make the variable public in the class. With a getter:
class Endnotes {
private $endnotes = array();
//replace any references to $endnotes in your class with $this->endnotes and add a function:
public function getEndnotes() {
return $this->endnotes;
}
}
//and then outside
print_r($replacer->getEndnotes());
preg_replace_callback doesn't pass by reference, so you aren't actually modifying the original string. $replacer->replace($body); should be $body = $replacer->replace($body); unless you want to pass body by reference into the replace() function and update its value there.

Error in php class with predefined values?

In my PHP file I have created a class as below but I am getting error on line 3rd and 5th line.
class CommonPath{
var $baseurl = 'http://mysite.com/';
var $docroot = realpath(dirname(__FILE__));
var $root = '/';
var $images = $this->root.'/img';
}
My Dreamwaver CS5 showing these lines (3rd & 5th) as erroneous lines and I am getting following error on executing this code.
Parse error: parse error, expecting `','' or `';'' in D:\wamp\www\site\libs\CommonPath.php on line 3
You can have only literals and constants as default values. No functions or other expressions are allowed.
There are two different mistakes. First, you cannot use functions to define class variables (line 3). Moreover, $this does not make sense in line 5, as you have got no object yet.
You can't assign the values like that right when you're declaring your member properties. Assign it in the constructor
class CommonPath{
var $baseurl = 'http://mysite.com/';
var $docroot = '';
var $root = '/';
var $images = '';
function __construct() {
$this->docroot = realpath(dirname(__FILE__));;
$this->images = $this->root.'/img';
}
}
You can not concat string and assign any value to variable which need to call any function, at the time declaring class variable.

Categories