I'm using a debugging aid in an application that uses var_dump()
with output buffering to capture variables and display them. However, I'm running into an issue with large objects that end up using up too much memory in the buffer.
function getFormattedOutput(mixed $var) { if (isTooLarge($var)) { return 'Too large! Abort!'; // What a solution *might* look like } ob_start(); var_dump($var); // Fatal error: Allowed memory size of 536870912 bytes exhausted $data = ob_get_clean(); // Return the nicely-formated data to use later return $data }
Is there a way I can prevent this? Or a work-around to detect that it's about to output a gigantic amount of info for a particular variable? I don't really have control which variables get passed into this function. It could be any type.
It's too simple. The var_dump() function displays structured information about variables/expressions including its type and value. Whereas The print_r() displays information about a variable in a way that's readable by humans. Example: Say we have got the following array and we want to display its contents.
var_dump() displays values along with data types as output. print_r() displays only value as output. It does not have any return type. It will return a value that is in string format.
The var_dump() function is used to dump information about a variable. This function displays structured information such as type and value of the given variable. Arrays and objects are explored recursively with values indented to show structure.
@JMTyler var_export returns a parsable string—essentially PHP code—while var_dump provides a raw dump of the data. So, for example, if you call var_dump on an integer with the value of 1, it would print int(1) while var_export just prints out 1 .
As all the others are mentioning what you ask is impossible. The only thing you can do is try to handle it as good as possible.
What you can try is to split it up into smaller pieces and then combine it. I've created a little test to try and get the memory error. Obviously a real world example might behave differently, but this seems to do the trick.
<?php define('mem_limit', return_bytes(ini_get('memory_limit'))); //allowed memory /* SIMPLE TEST CLASS */ class test { } $loop = 260; $t = new Test(); for ($x=0;$x<=$loop;$x++) { $v = 'test'.$x; $t->$v = new Test(); for ($y=0;$y<=$loop;$y++) { $v2 = 'test'.$y; $t->$v->$v2 = str_repeat('something to test! ', 200); } } /* ---------------- */ echo saferVarDumpObject($t); function varDumpToString($v) { ob_start(); var_dump($v); $content = ob_get_contents(); ob_end_clean(); return $content; } function saferVarDumpObject($var) { if (!is_object($var) && !is_array($var)) return varDumpToString($var); $content = ''; foreach($var as $v) { $content .= saferVarDumpObject($v); } //adding these smaller pieces to a single var works fine. //returning the complete larger piece gives memory error $length = strlen($content); $left = mem_limit-memory_get_usage(true); if ($left>$length) return $content; //enough memory left echo "WARNING! NOT ENOUGH MEMORY<hr>"; if ($left>100) { return substr($content, 0, $left-100); //100 is a margin I choose, return everything you have that fits in the memory } else { return ""; //return nothing. } } function return_bytes($val) { $val = trim($val); $last = strtolower($val[strlen($val)-1]); switch($last) { // The 'G' modifier is available since PHP 5.1.0 case 'g': $val *= 1024; case 'm': $val *= 1024; case 'k': $val *= 1024; } return $val; } ?>
UPDATE The version above still has some error. I recreated it to use a class and some other functions
As shown in the comments, the resource identifier for a class is different from the output of var_dump. As far as I can tell the other things are equal.
<?php /* RECURSION TEST */ class sibling { public $brother; public $sister; } $brother = new sibling(); $sister = new sibling(); $brother->sister = $sister; $sister->sister = $brother; Dump::Safer($brother); //simple class class test { } /* LARGE TEST CLASS - Many items */ $loop = 260; $t = new Test(); for ($x=0;$x<=$loop;$x++) { $v = 'test'.$x; $t->$v = new Test(); for ($y=0;$y<=$loop;$y++) { $v2 = 'test'.$y; $t->$v->$v2 = str_repeat('something to test! ', 200); } } //Dump::Safer($t); /* ---------------- */ /* LARGE TEST CLASS - Large attribute */ $a = new Test(); $a->t2 = new Test(); $a->t2->testlargeattribute = str_repeat('1', 268435456 - memory_get_usage(true) - 1000000); $a->smallattr1 = 'test small1'; $a->smallattr2 = 'test small2'; //Dump::Safer($a); /* ---------------- */ class Dump { private static $recursionhash; private static $memorylimit; private static $spacing; private static $mimicoutput = true; final public static function MimicOutput($v) { //show results similar to var_dump or without array/object information //defaults to similar as var_dump and cancels this on out of memory warning self::$mimicoutput = $v===false ? false : true; } final public static function Safer($var) { //set defaults self::$recursionhash = array(); self::$memorylimit = self::return_bytes(ini_get('memory_limit')); self::$spacing = 0; //echo output echo self::saferVarDumpObject($var); } final private static function saferVarDumpObject($var) { if (!is_object($var) && !is_array($var)) return self::Spacing().self::varDumpToString($var); //recursion check $hash = spl_object_hash($var); if (!empty(self::$recursionhash[$hash])) { return self::Spacing().'*RECURSION*'.self::Eol(); } self::$recursionhash[$hash] = true; //create a similar output as var dump to identify the instance $content = self::Spacing() . self::Header($var); //add some spacing to mimic vardump output //Perhaps not the best idea because the idea is to use as little memory as possible. self::$spacing++; //Loop trough everything to output the result foreach($var as $k=>$v) { $content .= self::Spacing().self::Key($k).self::Eol().self::saferVarDumpObject($v); } self::$spacing--; //decrease spacing and end the object/array $content .= self::Spacing().self::Footer().self::Eol(); //adding these smaller pieces to a single var works fine. //returning the complete larger piece gives memory error //length of string and the remaining memory $length = strlen($content); $left = self::$memorylimit-memory_get_usage(true); //enough memory left? if ($left>$length) return $content; //show warning trigger_error('Not enough memory to dump "'.get_class($var).'" memory left:'.$left, E_USER_WARNING); //stop mimic output to prevent fatal memory error self::MimicOutput(false); if ($left>100) { return substr($content, 0, $left-100); //100 is a margin I chose, return everything you have that fits in the memory } else { return ""; //return nothing. } } final private static function Spacing() { return self::$mimicoutput ? str_repeat(' ', self::$spacing*2) : ''; } final private static function Eol() { return self::$mimicoutput ? PHP_EOL : ''; } final private static function Header($var) { //the resource identifier for an object is WRONG! Its always 1 because you are passing around parts and not the actual object. Havent foundnd a fix yet return self::$mimicoutput ? (is_array($var) ? 'array('.count($var).')' : 'object('.get_class($var).')#'.intval($var).' ('.count((array)$var).')') . ' {'.PHP_EOL : ''; } final private static function Footer() { return self::$mimicoutput ? '}' : ''; } final private static function Key($k) { return self::$mimicoutput ? '['.(gettype($k)=='string' ? '"'.$k.'"' : $k ).']=>' : ''; } final private static function varDumpToString($v) { ob_start(); var_dump($v); $length = strlen($v); $left = self::$memorylimit-memory_get_usage(true); //enough memory left with some margin? if ($left-100>$length) { $content = ob_get_contents(); ob_end_clean(); return $content; } ob_end_clean(); //show warning trigger_error('Not enough memory to dump "'.gettype($v).'" memory left:'.$left, E_USER_WARNING); if ($left>100) { $header = gettype($v).'('.strlen($v).')'; return $header . substr($v, $left - strlen($header)); } else { return ""; //return nothing. } } final private static function return_bytes($val) { $val = trim($val); $last = strtolower($val[strlen($val)-1]); switch($last) { // The 'G' modifier is available since PHP 5.1.0 case 'g': $val *= 1024; case 'm': $val *= 1024; case 'k': $val *= 1024; } return $val; } } ?>
Well, if the physical memory is limited (you see the fatal error:)
Fatal error: Allowed memory size of 536870912 bytes exhausted
I would suggest to do the output buffering on disk (see callback parameter on ob_start
). Output buffering works chunked, that means, if there still is enough memory to keep the single chunk in memory, you can store it into a temporary file.
// handle output buffering via callback, set chunksize to one kilobyte ob_start($output_callback, $chunk_size = 1024);
However you must keep in mind that this will only prevent the fatal error while buffering. If you now want to return the buffer, you still need to have enough memory or you return the file-handle or file-path so that you can also stream the output.
However you can use that file then to obtain the size in bytes needed. The overhead for PHP strings is not much IIRC, so if there still is enough memory free for the filesize this should work well. You can substract offset to have a little room and play safe. Just try and error a little what it makes.
Some Example code (PHP 5.4):
<?php /** * @link http://stackoverflow.com/questions/5446647/how-can-i-use-var-dump-output-buffering-without-memory-errors/ */ class OutputBuffer { /** * @var int */ private $chunkSize; /** * @var bool */ private $started; /** * @var SplFileObject */ private $store; /** * @var bool Set Verbosity to true to output analysis data to stderr */ private $verbose = true; public function __construct($chunkSize = 1024) { $this->chunkSize = $chunkSize; $this->store = new SplTempFileObject(); } public function start() { if ($this->started) { throw new BadMethodCallException('Buffering already started, can not start again.'); } $this->started = true; $result = ob_start(array($this, 'bufferCallback'), $this->chunkSize); $this->verbose && file_put_contents('php://stderr', sprintf("Starting Buffering: %d; Level %d\n", $result, ob_get_level())); return $result; } public function flush() { $this->started && ob_flush(); } public function stop() { if ($this->started) { ob_flush(); $result = ob_end_flush(); $this->started = false; $this->verbose && file_put_contents('php://stderr', sprintf("Buffering stopped: %d; Level %d\n", $result, ob_get_level())); } } private function bufferCallback($chunk, $flags) { $chunkSize = strlen($chunk); if ($this->verbose) { $level = ob_get_level(); $constants = ['PHP_OUTPUT_HANDLER_START', 'PHP_OUTPUT_HANDLER_WRITE', 'PHP_OUTPUT_HANDLER_FLUSH', 'PHP_OUTPUT_HANDLER_CLEAN', 'PHP_OUTPUT_HANDLER_FINAL']; $flagsText = ''; foreach ($constants as $i => $constant) { if ($flags & ($value = constant($constant)) || $value == $flags) { $flagsText .= (strlen($flagsText) ? ' | ' : '') . $constant . "[$value]"; } } file_put_contents('php://stderr', "Buffer Callback: Chunk Size $chunkSize; Flags $flags ($flagsText); Level $level\n"); } if ($flags & PHP_OUTPUT_HANDLER_FINAL) { return TRUE; } if ($flags & PHP_OUTPUT_HANDLER_START) { $this->store->fseek(0, SEEK_END); } $chunkSize && $this->store->fwrite($chunk); if ($flags & PHP_OUTPUT_HANDLER_FLUSH) { // there is nothing to d } if ($flags & PHP_OUTPUT_HANDLER_CLEAN) { $this->store->ftruncate(0); } return ""; } public function getSize() { $this->store->fseek(0, SEEK_END); return $this->store->ftell(); } public function getBufferFile() { return $this->store; } public function getBuffer() { $array = iterator_to_array($this->store); return implode('', $array); } public function __toString() { return $this->getBuffer(); } public function endClean() { return ob_end_clean(); } } $buffer = new OutputBuffer(); echo "Starting Buffering now.\n=======================\n"; $buffer->start(); foreach (range(1, 10) as $iteration) { $string = "fill{$iteration}"; echo str_repeat($string, 100), "\n"; } $buffer->stop(); echo "Buffering Results:\n==================\n"; $size = $buffer->getSize(); echo "Buffer Size: $size (string length: ", strlen($buffer), ").\n"; echo "Peeking into buffer: ", var_dump(substr($buffer, 0, 10)), ' ...', var_dump(substr($buffer, -10)), "\n";
Output:
STDERR: Starting Buffering: 1; Level 1 STDERR: Buffer Callback: Chunk Size 1502; Flags 1 (PHP_OUTPUT_HANDLER_START[1]); Level 1 STDERR: Buffer Callback: Chunk Size 1503; Flags 0 (PHP_OUTPUT_HANDLER_WRITE[0]); Level 1 STDERR: Buffer Callback: Chunk Size 1503; Flags 0 (PHP_OUTPUT_HANDLER_WRITE[0]); Level 1 STDERR: Buffer Callback: Chunk Size 602; Flags 4 (PHP_OUTPUT_HANDLER_FLUSH[4]); Level 1 STDERR: Buffer Callback: Chunk Size 0; Flags 8 (PHP_OUTPUT_HANDLER_FINAL[8]); Level 1 STDERR: Buffering stopped: 1; Level 0 Starting Buffering now. ======================= Buffering Results: ================== Buffer Size: 5110 (string length: 5110). Peeking into buffer: string(10) "fill1fill1" ...string(10) "l10fill10\n"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With