Php 如何使用var#u dump+;没有内存错误的输出缓冲?

Php 如何使用var#u dump+;没有内存错误的输出缓冲?,php,memory,Php,Memory,我正在一个应用程序中使用调试辅助工具,该应用程序使用带有输出缓冲的var\u dump()来捕获变量并显示它们。但是,我遇到了一个问题,即大型对象最终会占用缓冲区中的太多内存 function getFormattedOutput(mixed $var) { if (isTooLarge($var)) { return 'Too large! Abort!'; // What a solution *might* look like } ob_start(); var

我正在一个应用程序中使用调试辅助工具,该应用程序使用带有输出缓冲的
var\u dump()
来捕获变量并显示它们。但是,我遇到了一个问题,即大型对象最终会占用缓冲区中的太多内存

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
}

有什么办法可以防止这种情况发生吗?或者是一种变通方法来检测它即将为一个特定变量输出大量信息?我无法控制哪些变量被传递到这个函数中。它可以是任何类型

在所有xdebug中插入时,可以限制变量转储跟随对象的深度。在某些软件产品中,您可能会遇到一种递归,它会使var_dump的输出膨胀。 除此之外,您还可以提高内存限制


请看

我很抱歉,但我认为你的问题没有解决办法。您要求确定一个大小,以防止为该大小分配内存。PHP无法回答“它将消耗多少内存”,因为ZVAL结构是在PHP中使用时创建的。有关PHP内存分配内部结构的概述,请参阅

你给出了正确的提示“里面可能有任何东西”,从我的观点来看,这就是问题所在。有一个架构问题导致了您描述的案例。我认为你试图在错误的一端解决它

例如:您可以从php中每种类型的开关开始,尝试为每种大小设置限制。只要没有人想到改变进程中的内存限制,这种情况就会持续

Xdebug是一个很好的解决方案,因为它可以防止应用程序因日志功能(甚至非业务关键)而爆炸,而且它是一个不好的解决方案,因为您不应该在生产中激活Xdebug

我认为内存异常是正确的行为,您不应该试图解决它


[rant]如果转储50兆字节或更多字符串的人不关心他/她的应用程序行为,他/她就应该受到影响;)[/rant]

我不相信有任何方法可以确定一个特定函数最终会占用多少内存。您可以做的一件事是在设置
$largeVar
之前检查脚本当前占用的内存量,然后将其与设置之后的内存量进行比较。这将使您对
$largeVar
的大小有一个很好的了解,并且您可以在优雅地退出之前运行测试以确定可接受的最大大小限制


您还可以自己重新实现var_dump()函数。让函数遍历结构并在生成时回显结果内容,或者将其存储在临时文件中,而不是在内存中存储一个巨大的字符串。这将允许您获得相同的期望结果,但不会出现您遇到的内存问题。

因为所有其他人都提到了您所要求的内容,这是不可能的。你唯一能做的就是尽可能地处理好它

你可以试着把它分成小块,然后再组合起来。我创建了一个小测试来尝试获取内存错误。很明显,现实世界中的示例可能会有不同的行为,但这似乎起到了作用

<?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;
}
?>

更新 上面的版本仍然有一些错误。我重新创建它以使用一个类和一些其他函数

  • 检查递归
  • 修复单个大属性
  • 模拟var_转储输出
  • 触发警告时的错误,以便能够捕获/隐藏它
如注释所示,类的资源标识符不同于var_dump的输出。据我所知,其他事情都是一样的

<?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;
  }
}
?>

如果物理内存受到限制(您会看到致命错误:)

致命错误:允许的内存大小536870912字节已用尽

我建议在磁盘上执行输出缓冲(请参阅上的回调参数)。输出缓冲是分块工作的,这意味着,如果内存中仍然有足够的内存来保存单个块,则可以将其存储到临时文件中

// handle output buffering via callback, set chunksize to one kilobyte
ob_start($output_callback, $chunk_size = 1024);
但是,您必须记住,这只会在缓冲时防止致命错误。如果现在要返回缓冲区,则仍然需要有足够的内存,或者返回文件句柄或文件路径,以便还可以对输出进行流式处理

但是,您可以使用该文件获得所需的大小(以字节为单位)。PHP字符串的开销不是很多IIRC,因此如果仍然有足够的内存用于文件大小,这应该可以很好地工作。您可以减去偏移量,以留出一点空间并确保安全。只要试着把它弄错一点就行了

一些示例代码(PHP5.4):


出于好奇,您是否会在打印时遇到同样的问题?如果没有,你会看到很多递归通知吗?@Charles,可能不会。我可以使用
print\u r
var\u export
,但我非常喜欢这样一个事实:我可以保留
var\u dump
提供的变量类型和长度信息。另外,当xdebug可用时,它还增加了格式化的好处。由于递归,可能会产生无限量的输出。试着自己在同一个var上调用它,而不使用输出缓冲,看看会发生什么。@Jon:
var\u dump
可以解析递归引用。@Nelson因为php使用了写时拷贝,这会有什么帮助,因为我没有更改$var?我很感激这篇文章,但包含“减少转储”的答案实际上只解决了这个问题,不是问题。这个函数可以有一个10维数组,每个数组中只有一个元素,可以转储。但是,具有其他大型对象属性的类可能会在第二级引发问题。我意识到我所问的可能是不可能的,可能只是你有多少物理内存的问题这应该是正确的答案。如果这是不可能的,那么你的方法就有问题了……很有趣。但是,如果对象属性过大,这将如何表现呢?尝试过但失败了
<?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";
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"