PHP resources cannot be identified from backtrace (call stack) - php

I'm writing PHP method to pretty-print callstack with params. Reason for this is it will be used as an output of a public API (in debug mode) so it must not display everything and only display save information.
I would like to see something like this:
Config->saveToFile(resource: file) in config.php::456
Config->backup('config.bak') in config.php::123
But when I call debug_backtrace() and parse the args value, I cannot use methods gettype(), is_resource() and get_resource_type() because it always say the variable is of unknown type:
Config->saveToFile(Unknown type: Resource id #99) in config.php::456
Config->backup('config.bak') in config.php::123
Code used to parse args is:
public static function getTrace() {
$trace = debug_backtrace();
$output = [];
foreach ($trace as $call) {
$name = $call['class'] . $call['type'] . $call['function']
//actual code checks for various situations
$args = [];
foreach ($call['args'] as $arg) {
$args[] = self::toString($arg);
}
$name .= '(' . join(', ', $args) . ')';
$output[] = $name . ' in ' . basename($call['file']) . '::' . $call['line'];
}
return $output;
}
protected static function toString($mixed) {
//process known types - arrays, objects, strings, etc.
//...
if (is_resource($mixed)) {
return 'resource: ' . get_resource_type($mixed);
}
return gettype($mixed) . ': ' . $mixed;
}
Even when I use code by diz at ysagoon dot com listed under debug_backtrace documentation, which utilize gettype() and check for resource, in my case it returns Config->saveToFile(Unknown).
When I use the methods in code where the resource is created, it correctly returns its type.
Is there a limit or reason why resources are not identified from backtrace? Something I should enable in PHP configuration? I haven't found anything about this in PHP documentation nor Google.
System:
XAMPP 3.2.2
Apache/2.4.17 (Win32)
PHP/5.6.15
Windows 10 Pro x64 Anniversary edition 1607 (10.0.14393)

So the problem is that resources can be identified as a resource only while they are opened. After you close the resource, it is no more identified by methods gettype(), is_resource() and get_resource_type() as a resource and instead change to unknown type.
$f = fopen('tmp', 'w');
echo gettype($f); //= 'resource'
fclose($f);
echo gettype($f); //= 'Unknown type'
To print closed resources in backtrace I've created two methods to remember resources while they are still opened:
protected $resources = [];
public function traceResourceParams() {
$trace = debug_backtrace();
$args = [];
foreach ($trace as $call) {
foreach ($call['args'] as $arg) {
if (is_resource($arg) && !array_key_exists(intval($arg), $this->resources)) {
$this->resources[intval($arg)] = self::toString($arg);
}
}
}
}
public function traceNamedResource($resource, $name) {
if (is_resource($resource)) {
$this->resources[intval($resource)] = '{' . get_resource_type($resource) . ': ' . $name . '}';
}
}
And updated my toString method to check for stored resources:
protected static function toString($mixed) {
//process known types - arrays, objects, strings, etc.
//...
if (is_resource($mixed)) {
return 'resource: ' . get_resource_type($mixed);
}
//closed resources does not evaluate as resource
//but still convert to resource id using intval()
//so we can match them to previously evaluated resources
$val = intval($mixed);
if ($val && array_key_exists($val, self::getInstance()->resources)) {
return self::getInstance()->resources[$val];
}
return gettype($mixed) . ': ' . $mixed;
}
So now I can store the resource when it is created:
$f = fopen('tmp', 'w');
$debug->traceNamedResource($f, 'tmp');
fclose($f);
Or when it is passed as a parameter:
protected function saveToFile($file) {
$debug->traceResourceParams()
//... work with file
fclose($file);
}

Related

Magic Constants from called file - not action file

I have this function in my class:
logMagic($mode)
{
# mode
# 1 = all, 2 = dir, 3 = file etc.
# this is wrapped inside a switch statement
# for eases sake here's the case 1: code
$log['dir'] = 'DIRECTORY: '. __DIR__;
$log['file'] = 'FILE: '. __FILE__;
$log['meth'] = 'METHOD: '. __METHOD__;
$log['fnc'] = 'FUNCTION: '. __FUNCTION__;
$log['ns'] = 'NAMESPACE: '. __NAMESPACE__;
$log['cl'] = 'CLASS: '. __CLASS__;
return $log;
}
This is in a foo.php file. I then have a bar.php file where I call and init the class to use this function:
require_once 'foo.php';
$logger = new \Logger('trey.log', 'var/logs');
$logger->logMagic($logger::ALL);
My problem with this is, this will output (in a log file):
DIRECTORY: /var/www/dir
FILE: /var/www/dir/foo.php
METHOD: Logger::logMagic
FUNCTION: logMagic
NAMESPACE:
CLASS: Logger
My expected output was that it would return
DIRECTORY: /var/www/dir
FILE: /var/www/dir/bar.php
METHOD:
FUNCTION:
NAMESPACE:
CLASS:
Reading the docs does clarify this to me that this is normal.
Is there any way I can use magic constants from fileb.php in filea.php, without passing params to the function?
Thanks to the pos dupe link I managed to do some digging to really get what I want. It seems with debug_backtrace() it well.. traces back through each function call. E.g.
fileA.php
class Bar
{
public function foo()
{
echo '<pre>'. print_r(debug_backtrace(), 1) .'</pre>';
return 'hi';
}
}
fileB.php
require_once 'fileA.php';
$bar = new \Bar();
echo $bar->foo();
This outputs:
Array
(
[0] => Array
(
[file] => /var/www/testing/test/fileB.php
[line] => 5
[function] => foo
[class] => Bar
[object] => Bar Object ()
[type] => ->
[args] => Array ()
)
)
hi
This is for the most part, perfect. However, this doesn't gurantee results as the array increases per stack.
E.g. FileC.php calls function in FileB.php which in turn, calls a function in
FileA.php
However, I noted with use of the function that the most desirable one is the end element in the array. With that in mind, I've set up a few functions to mimic functionality of the magic constants, without using any magic.
Set up for use of functions:
$trace = debug_backtrace();
$call = end($trace);
Directory (__DIR__):
# $trace = $call['file']
protected function getDir($trace)
{
$arr = explode('/', $trace);
$file = end($arr);
$directory = [];
$i = 0;
foreach ($arr as $data)
{
if ($data !== $file) {
$directory[] = isset($output) ? $output[$i - 1] . '/' . $data : $data;
$i++;
}
}
return 'DIRECTORY: '. implode('/', $directory);
}
File (__FILE__)::
# $trace = $call['file']
protected function getFile($trace)
{
$arr = explode('/', $trace);
$file = end($arr);
return 'FILE: '. $file;
}
Function/Method (__FUNCTION__ || __METHOD__)::
# $trace = $call
protected function getFunction($trace)
{
$output = 'FUNCTION: '. $trace['function'] ."\n";
foreach ($trace['args'] as $key => $arguments)
{
foreach ($arguments as $k => $arg)
{
if (!is_array($arg)) {
$output .= 'ARGS ('. $k .'): '. $arg ."\n";
}
}
}
return $output;
}
Namespace (__NAMESPACE__):
# $trace = $call['class']
protected function getNamespace($trace)
{
$arr = explode('\\', $trace);
$class = end($arr);
$namespace = [];
$i = 0;
foreach ($arr as $data)
{
if ($data !== $class) {
$namespace[] = isset($output) ? $output[$i - 1] . '/' . $data : $data;
$i++;
}
}
return 'NAMESPACE: '. implode('\\', $namespace);
}
Class (__CLASS__):
# $trace = $call['class']
protected function logClass($trace)
{
if (strpos($trace, '\\') !== false) {
$arr = explode('\\', $trace);
$class = end($arr);
} else {
$class = $trace;
}
$return = 'CLASS: '. $class;
}
Missing Magic Constants:
__LINE__
__TRAIT__
Line is accessible (as you'll see from print_r($call, 1)) but I wasn't in need/interested. Trait is more or less the same as __NAMESPACE__ in my uses, so again, it wasn't interested in creating a function for it.
Notes:
This is part of a class I made that makes use of the protected function via public accessible functions - please ignore :)
These functions could be cleaned up (e.g. instead of $trace = $call['file'], use $file as param)

Can I use PHP anonymous function as an argument, without assigning the function to a variable?

Does PHP allow the use of an anonymous function as one of the arguments during concatenation?
If so, what is the proper syntax?
For example, here's an example of what I want to get to work:
$final_text = $some_initial_string . function ($array_of_strings)
{
$out = '';
foreach ($array_of_strings as $this_particular_string)
{
$out .= $this_particular_string;
}
return $out;
};
Note: the below is expected to work for PHP Version 7.x but does not work on PHP Version 5.6 (For 5.6, first assign the anonymous function to a variable)
/*
* Strings before & after
*/
$table_heading_text = "HEADING";
$table_bottom_text = "BOTTOM";
/*
* Use the function this way
*/
echo $table_heading_text . (function (array $array_of_strings)
{
$out = '';
foreach ($array_of_strings as $this_particular_string)
{
$out .= $this_particular_string;
}
return $out;
})(array(
"hi",
"mom"
)) . $table_bottom_text;
In short ...
function must return some value that can be converted to text
function definition must be enclosed in parenthesis ( ... )
Don't forget to have calling arguments after the function definition
Examples:
echo "BEFORE" . (function ($x){return $x;})(" - MIDDLE - ") . "AFTER";
echo "BEFORE" . (function (){return " - MIDDLE - ";})() . "AFTER";
Also, using implode() may be better for this particular task.

How to create an array tree from boolean notation (DSL) with PHP

My input is quite simple:
$input = '( ( "M" AND ( "(" OR "AND" ) ) OR "T" )';
where ( starts a new node on tree and ) ends it. AND and OR words are reserved for boolean operation so until they are not inside "" marks, they have a special meaning. In my DSL AND and OR clauses alter By node level so that there can be only either AND or OR clauses at level. If AND comes after OR, it will start a new subnode. All characters inside "" should be regarded as they are. Finally " could be escaped with \" as usual.
What is a good way to make translate sentence which look like this in PHP:
$output = array(array(array("M" , array("(", "AND")) , "T"), FALSE);
Note that FALSE is an indicator, that the root level had OR keyword. If input was:
( ( "M" AND ( "(" OR "AND" ) ) AND "T" )
then output would be:
$output = array(array(array("M", array("(", "AND")), "T"), TRUE);
It is tempting to use replace('(', 'array('); and eval code, but then escaping characters and wrapping literals would become an issue.
At the moment I'm not implementing NOT boolean operator on DSL.
Thanks for any help. JavaSript code are ok too.
Python example:
I made some tests with Python before going to PHP and Javascript. What I did was:
find string literals with regex
replace literals with generated keys
store literals to assoc list
split input to single level list by parenthesis
find root level boolean operator
get rid of boolean operators and white space
replace literal keys with stored values
It might work but I'm sure there must be much more sophisticated way to do it.
http://codepad.org/PdgQLviI
Here's my side-project's library modification. It should handle these kind of strings - perform some stress tests and let me know if it breaks somewhere.
Tokenizer type class is needed to extract and tokenize variables, so they don't interfere with syntax parsing and tokenize parethesis so they could be matched directly (lazy-evaluated content wouldn't catch nested level and greedy would cover all contexts on the same level). It also has some keyword syntax (a little more than needed, since it will be parsed only for root level). Throws InvalidArgumentException when trying to access variables registry with wrong key, and RuntimeException when parenthesis don't match.
class TokenizedInput
{
const VAR_REGEXP = '\"(?P<string>.*?)\"';
const BLOCK_OPEN_REGEXP = '\(';
const BLOCK_CLOSE_REGEXP = '\)';
const KEYWORD_REGEXP = '(?<keyword>OR|AND)';
// Token: <TOKEN_DELIM_LEFT><TYPE_TOKEN><ID_DELIM>$id<TOKEN_DELIM_RIGHT>
const TOKEN_DELIM_LEFT = '<';
const TOKEN_DELIM_RIGHT = '>';
const VAR_TOKEN = 'VAR';
const KEYWORD_TOKEN = 'KEYWORD';
const BLOCK_OPEN_TOKEN = 'BLOCK';
const BLOCK_CLOSE_TOKEN = 'ENDBLOCK';
const ID_DELIM = ':';
const ID_REGEXP = '[0-9]+';
private $original;
private $tokenized;
private $data = [];
private $blockLevel = 0;
private $varTokenId = 0;
protected $procedure = [
'varTokens' => self::VAR_REGEXP,
'keywordToken' => self::KEYWORD_REGEXP,
'blockTokens' => '(?P<open>' . self::BLOCK_OPEN_REGEXP . ')|(?P<close>' . self::BLOCK_CLOSE_REGEXP . ')'
];
private $tokenMatch;
public function __construct($input) {
$this->original = (string) $input;
}
public function string() {
isset($this->tokenized) or $this->tokenize();
return $this->tokenized;
}
public function variable($key) {
isset($this->tokenized) or $this->tokenize();
if (!isset($this->data[$key])) {
throw new InvalidArgumentException("Variable id:($key) does not exist.");
}
return $this->data[$key];
}
public function tokenSearchRegexp() {
if (!isset($this->tokenMatch)) {
$strings = $this->stringSearchRegexp();
$blocks = $this->blockSearchRegexp();
$this->tokenMatch = '#(?:' . $strings . '|' . $blocks . ')#';
}
return $this->tokenMatch;
}
public function stringSearchRegexp($id = null) {
$id = $id ?: self::ID_REGEXP;
return preg_quote(self::TOKEN_DELIM_LEFT . self::VAR_TOKEN . self::ID_DELIM)
. '(?P<id>' . $id . ')'
. preg_quote(self::TOKEN_DELIM_RIGHT);
}
public function blockSearchRegexp($level = null) {
$level = $level ?: self::ID_REGEXP;
$block_open = preg_quote(self::TOKEN_DELIM_LEFT . self::BLOCK_OPEN_TOKEN . self::ID_DELIM)
. '(?P<level>' . $level . ')'
. preg_quote(self::TOKEN_DELIM_RIGHT);
$block_close = preg_quote(self::TOKEN_DELIM_LEFT . self::BLOCK_CLOSE_TOKEN . self::ID_DELIM)
. '\k<level>'
. preg_quote(self::TOKEN_DELIM_RIGHT);
return $block_open . '(?P<contents>.*)' . $block_close;
}
public function keywordSearchRegexp($keyword = null) {
$keyword = $keyword ? '(?P<keyword>' . $keyword . ')' : self::KEYWORD_REGEXP;
return preg_quote(self::TOKEN_DELIM_LEFT . self::KEYWORD_TOKEN . self::ID_DELIM)
. $keyword
. preg_quote(self::TOKEN_DELIM_RIGHT);
}
private function tokenize() {
$current = $this->original;
foreach ($this->procedure as $method => $pattern) {
$current = preg_replace_callback('#(?:' . $pattern . ')#', [$this, $method], $current);
}
if ($this->blockLevel) {
throw new RuntimeException("Syntax error. Parenthesis mismatch." . $this->blockLevel);
}
$this->tokenized = $current;
}
protected function blockTokens($match) {
if (isset($match['close'])) {
$token = self::BLOCK_CLOSE_TOKEN . self::ID_DELIM . --$this->blockLevel;
} else {
$token = self::BLOCK_OPEN_TOKEN . self::ID_DELIM . $this->blockLevel++;
}
return $this->addDelimiters($token);
}
protected function varTokens($match) {
$this->data[$this->varTokenId] = $match[1];
return $this->addDelimiters(self::VAR_TOKEN . self::ID_DELIM . $this->varTokenId++);
}
protected function keywordToken($match) {
return $this->addDelimiters(self::KEYWORD_TOKEN . self::ID_DELIM . $match[1]);
}
private function addDelimiters($token) {
return self::TOKEN_DELIM_LEFT . $token . self::TOKEN_DELIM_RIGHT;
}
}
Parser type class performs matching on tokenized string - pulls out registered variables and goes recursively into nested contexts by clonig itself.
Operator type handling is unusual, which makes it more of a derived class, but it's hard to achieve satysfying abstraction in Parsers' world anyway.
class ParsedInput
{
private $input;
private $result;
private $context;
public function __construct(TokenizedInput $input) {
$this->input = $input;
}
public function result() {
if (isset($this->result)) { return $this->result; }
$this->parse($this->input->string());
$this->addOperator();
return $this->result;
}
private function parse($string, $context = 'root') {
$this->context = $context;
preg_replace_callback(
$this->input->tokenSearchRegexp(),
[$this, 'buildStructure'],
$string
);
return $this->result;
}
protected function buildStructure($match) {
if (isset($match['contents'])) { $this->parseBlock($match['contents'], $match['level']); }
elseif (isset($match['id'])) { $this->parseVar($match['id']); }
}
protected function parseVar($id) {
$this->result[] = $this->input->variable((int) $id);
}
protected function parseBlock($contents, $level) {
$nested = clone $this;
$this->result[] = $nested->parse($contents, (int) $level);
}
protected function addOperator() {
$subBlocks = '#' . $this->input->blockSearchRegexp(1) . '#';
$rootLevel = preg_replace($subBlocks, '', $this->input->string());
$rootKeyword = '#' . $this->input->keywordSearchRegexp('AND') . '#';
return $this->result[] = (preg_match($rootKeyword, $rootLevel) === 1);
}
public function __clone() {
$this->result = [];
}
}
Example usage:
$input = '( ( "M" AND ( "(" OR "AND" ) ) AND "T" )';
$tokenized = new TokenizedInput($input);
$parsed = new ParsedInput($tokenized);
$result = $parsed->result();
I removed namespaces/imports/intrefaces, so you might adjust'em as you need. Also didn't want to dig through (possibly invalid now) comments, so removed them as well.

How to keep memory low when using a hierarchical object structure

I have a simple object thing that is able to have children of the same type.
This object has a toHTML method, which does something like:
$html = '<div>' . $this->name . '</div>';
$html .= '<ul>';
foreach($this->children as $child)
$html .= '<li>' . $child->toHTML() . '</li>';
$html .= '</ul>';
return $html;
The problem is that when the object is complex, like lots of children with children with children etc, memory usage skyrockets.
If I simply print_r the multidimensional array that feeds this object I get like 1 MB memory usage, but after I convert the array to my object and do print $root->toHtml() it takes 10 MB !!
How can I fix this?
====================================
Made a simple class that is similar to my real code (but smaller):
class obj{
protected $name;
protected $children = array();
public function __construct($name){
$this->name = $name;
}
public static function build($name, $array = array()){
$obj = new self($name);
if(is_array($array)){
foreach($array as $k => $v)
$obj->addChild(self::build($k, $v));
}
return $obj;
}
public function addChild(self $child){
$this->children[] = $child;
}
public function toHTML(){
$html = '<div>' . $this->name . '</div>';
$html .= '<ul>';
foreach($this->children as $child)
$html .= '<li>' . $child->toHTML() . '</li>';
$html .= '</ul>';
return $html;
}
}
And tests:
$big = array_fill(0, 500, true);
$big[5] = array_fill(0, 200, $big);
print_r($big);
// memory_get_peak_usage() shows 0.61 MB
$root = obj::build('root', $big);
// memory_get_peak_usage() shows 18.5 MB wtf lol
print $root->toHTML();
// memory_get_peak_usage() shows 24.6 MB
The problem is that you're buffering all the data in memory, which you don't actually need to do, as you're just outputting the data, rather than actually processing it.
Rather than buffering everything in memory, if all you want to do is output it you should just output it to wherever it's going to:
public function toHTMLOutput($outputStream){
fwrite($outputStream, '<div>' . $this->name . '</div>';
fwrite($outputStream, '<ul>');
foreach($this->children as $child){
fwrite($outputStream, '<li>');
$child->toHTMLOutput($outputStream);
fwrite($outputStream, '</li>');}
}
fwrite($outputStream, '</ul>');
}
$stdout = fopen('php://stdout', 'w');
print $root->toHTMLOutput($stdout);
or if you want to save the output to a file
$stdout = fopen('htmloutput.html', 'w');
print $root->toHTMLOutput($stdout);
Obviously I've only implemented it for the toHTML() function but the same principle should be done for the build function, which could lead to you skipping a separate toHTML function at all.
Introduction
Since you are sill going to output the HTML there is no need to save it indirectly consuming memory.
Here is a simple class that :
Builds menu from multidimensional array
Memory efficient uses Iterator
Can Write to Socket , Stream , File , array , Iterator etc
Example
$it = new ListBuilder(new RecursiveArrayIterator($big));
// Use Echo
$m = memory_get_peak_usage();
$it->display();
printf("%0.5fMB\n", (memory_get_peak_usage() - $m) / (1024 * 1024));
Output
0.03674MB
Other Output Interfaces
$big = array_fill(0, 500, true);
$big[5] = array_fill(0, 200, $big);
Simple Compare
// Use Echo
$m = memory_get_peak_usage();
$it->display();
$responce['echo'] = sprintf("%0.5fMB\n", (memory_get_peak_usage() - $m) / (1024 * 1024));
// Output to Stream or File eg ( Socket or HTML file)
$m = memory_get_peak_usage();
$it->display(fopen("php://output", "w"));
$responce['stream'] = sprintf("%0.5fMB\n", (memory_get_peak_usage() - $m) / (1024 * 1024));
// Output to ArrayIterator
$m = memory_get_peak_usage();
$it->display($array = new ArrayIterator());
$responce['iterator'] = sprintf("%0.5fMB\n", (memory_get_peak_usage() - $m) / (1024 * 1024));
// Output to Array
$m = memory_get_peak_usage();
$it->display($array = []);
$responce['array'] = sprintf("%0.5fMB\n", (memory_get_peak_usage() - $m) / (1024 * 1024));
echo "\n\nResults \n";
echo json_encode($responce, 128);
Output
Results
{
"echo": "0.03684MB\n",
"stream": "0.00081MB\n",
"iterator": "32.04364MB\n",
"array": "0.00253MB\n"
}
Class Used
class ListBuilder extends RecursiveIteratorIterator {
protected $pad = "\t";
protected $o;
public function beginChildren() {
$this->output("%s<ul>\n", $this->getPad());
}
public function endChildren() {
$this->output("%s</ul>\n", $this->getPad());
}
public function current() {
$this->output("%s<li>%s</li>\n", $this->getPad(1), parent::current());
return parent::current();
}
public function getPad($n = 0) {
return str_repeat($this->pad, $this->getDepth() + $n);
}
function output() {
$args = func_get_args();
$format = array_shift($args);
$var = vsprintf($format, $args);
switch (true) {
case $this->o instanceof ArrayIterator :
$this->o->append($var);
break;
case is_array($this->o) || $this->o instanceof ArrayObject :
$this->o[] = $var;
break;
case is_resource($this->o) && (get_resource_type($this->o) === "file" || get_resource_type($this->o) === "stream") :
fwrite($this->o, $var);
break;
default :
echo $var;
break;
}
}
function display($output = null) {
$this->o = $output;
$this->output("%s<ul>\n", $this->getPad());
foreach($this as $v) {
}
$this->output("%s</ul>\n", $this->getPad());
}
}
Conclusion
As you can see looping with iterator is fast but store values in iterator or object might not be that memory efficient.
Total number of elements in Your array is a little over 100000.
Each element of Your array is just one byte (boolean) so for over 100000 elements it takes 100000bytes ~0.1MB
Each of Your objects is ~100 bytes it is 100*100000 = 100000000 bytes ~ 10MB
But You have ~18MB so where is this 8 from?
If You run this code
<?php
$c = 0; //we use this to count object isntances
class obj{
protected $name;
protected $children = array();
public static $c=0;
public function __construct($name){
global $c;
$c++;
$this->name = $name;
}
public static function build($name, $array = array()){
global $c;
$b = memory_get_usage();
$obj = new self($name);
$diff = memory_get_usage()-$b;
echo $c . ' diff ' . $diff . '<br />'; //display change in allocated size
if(is_array($array)){
foreach($array as $k => $v)
$obj->addChild(self::build($k, $v));
}
return $obj;
}
public function addChild(self $child){
$this->children[] = $child;
}
public function toHTML(){
$html = '<div>' . $this->name . '</div>';
$html .= '<ul>';
foreach($this->children as $child)
$html .= '<li>' . $child->toHTML() . '</li>';
$html .= '</ul>';
return $html;
}
}
$big = array_fill(0, 500, true);
$big[5] = array_fill(0, 200, $big);
$root = obj::build('root', $big);
You will notice a change is constant with exception for objects created as
1024th, 2048th, 4096th...
I don't have link to any article or manual page about it but my guess is that php hold references to each created object in array with initial size of 1024. When You make this array full its size will get doubled to make space for new objects.
If You take difference from for example 2048th object subtract a size of object( the constant value You have in other lines) and divide by 2048 You will always get 32 - standard size of pointer in C.
So for 100000 objects this array grown to size of 131072 elements.
131072*32 = 4194304B = 4MB
This calculation are just approximate but I think it answers Your question what takes so much memory.
To answer how to keep memory low - avoid using objects for large set of data.
Obviously objects are nice and stuff but primitive data types are faster and smaller.
Maybe You can make it work with one object containing array with data. Hard to propose any alternative without more info about this objects and what methods/interface they require.
One thing that might be catching you is that you might be getting close to blowing your stack because of recursion. It might make sense in this case to create a rendering function that deals with the tree as a whole to render instead of relying on recursion to do the rendering for you. For informative topics on this see tail call recursion and tail call optimization.
To stick with your code's current structure and dodge a lot of the resource problems that you are likely facing the simplest solution may be to simply pass in the html string as a reference like:
class obj{
protected $name;
protected $children = array();
public function __construct($name){
$this->name = $name;
}
public static function build($name, $array = array()){
$obj = new self($name);
if(is_array($array)){
foreach($array as $k => $v)
$obj->addChild(self::build($k, $v));
}
return $obj;
}
public function addChild(self $child){
$this->children[] = $child;
}
public function toHTML(&$html = ""){
$html .= '<div>' . $this->name . '</div>';
$html .= '<ul>';
foreach($this->children as $child){
$html .= '<li>';
$html .= $child->toHTML($html);
$html .= '</li>';
}
$html .= '</ul>';
}
}
This will keep you from hauling around a bunch of duplicate partial tree renders while the recursive calls are resolving.
As for the actual build of the tree I think a lot of the memory usage is just the price of playing with data that big, your options there are either render instead of building up a hierarchical model just to render (just render output instead of building a tree) or, to employ some sort of caching strategies to either cache copies of the object tree or copies of the rendered html depending on how the data is used within your site. If you have control of the inbound data invalidating relevant cache keys can be added to that work flow to keep the cache from getting stale.

How to convert SelectQuery object to SQL string?

I managed to print a string using __toString() magic method, but in this string I see placeholders (for conditions params), and it doesn't work as SQL query.
I checked documentation of this object, and also looked in google, but couldn't find a working answer.
Basing on question's comments (thanks #Scuzzy for inspiration) I wrote some simple piece of code to convert SelectQuery object:
class ExportableSelectQuery {
public static function toSql(SelectQuery $obj) {
$_string = $obj->__toString();
$_conditions = $obj->conditions();
$_tables = $obj->getTables();
$_fields = $obj->getFields();
foreach($_tables as $k => $t) {
if(!empty($t['alias'])) {
$_string = str_replace('{' . $t['table'] . '}', $t['table'] . ' as', $_string);
}
else {
$_string = str_replace('{' . $t['table'] . '}', $t['table'], $_string);
}
}
foreach($_conditions as $k => $c) {
if(is_int($c['value'])) {
$_string = str_replace(':db_condition_placeholder_' . $k, $c['value'], $_string);
}
else {
$_string = str_replace(':db_condition_placeholder_' . $k, "'" . $c['value'] . "'", $_string);
}
}
//echo('<pre>');
//var_dump($_fields);
//var_dump($_conditions);
//var_dump($_tables);
//var_dump($_string);
//echo('</pre>');
//die();
return $_string;
}
}
Usage of this code is now simple (if you only have SelectQuery object somewhere):
die(ExportableSelectQuery::toSql($query));
I was thinking about extending original SelectQuery object, and provide method to get SQL code, but Drupal's db_select function returns SelectQuery, so I will have to either change db_select function or cast returned object to ExportableSelectQuery.
Also this is not probably best solution I could write, but assuming limit of time and purpose it solved my problem just fine.
If you wish to get SQL from for example "EntityFieldQyery", you may use something like this
Add tag to query
$query->entityCondition('entity_type', 'node')
->entityCondition('bundle', 'page')
->addTag('EFQDumper'); //<=== TAG
Implement hook "query_TAG_alter"
function YOURMODULE_query_EFQDumper_alter(QueryAlterableInterface $query)
{
//echo ExportableSelectQuery::toSql($query);
//die();
}
The solution based on Carlos comment

Categories