├── .gitattributes ├── .gitignore ├── README.md ├── composer.json ├── ref.css ├── ref.js ├── ref.php └── tests ├── example.class.php ├── file.txt ├── index.php └── today /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | [Dd]ebug/ 46 | [Rr]elease/ 47 | *_i.c 48 | *_p.c 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.vspscc 63 | .builds 64 | *.dotCover 65 | 66 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 67 | #packages/ 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | 76 | # Visual Studio profiler 77 | *.psess 78 | *.vsp 79 | 80 | # ReSharper is a .NET coding add-in 81 | _ReSharper* 82 | 83 | # Installshield output folder 84 | [Ee]xpress 85 | 86 | # DocProject is a documentation generator add-in 87 | DocProject/buildhelp/ 88 | DocProject/Help/*.HxT 89 | DocProject/Help/*.HxC 90 | DocProject/Help/*.hhc 91 | DocProject/Help/*.hhk 92 | DocProject/Help/*.hhp 93 | DocProject/Help/Html2 94 | DocProject/Help/html 95 | 96 | # Click-Once directory 97 | publish 98 | 99 | # Others 100 | [Bb]in 101 | [Oo]bj 102 | sql 103 | TestResults 104 | *.Cache 105 | ClientBin 106 | stylecop.* 107 | ~$* 108 | *.dbmdl 109 | Generated_Code #added for RIA/Silverlight projects 110 | 111 | # Backup & report files from converting an old project file to a newer 112 | # Visual Studio version. Backup files are not needed, because we have git ;-) 113 | _UpgradeReport_Files/ 114 | Backup*/ 115 | UpgradeLog*.XML 116 | 117 | 118 | 119 | ############ 120 | ## Windows 121 | ############ 122 | 123 | # Windows image file caches 124 | Thumbs.db 125 | 126 | # Folder config file 127 | Desktop.ini 128 | 129 | 130 | ############# 131 | ## Python 132 | ############# 133 | 134 | *.py[co] 135 | 136 | # Packages 137 | *.egg 138 | *.egg-info 139 | dist 140 | build 141 | eggs 142 | parts 143 | bin 144 | var 145 | sdist 146 | develop-eggs 147 | .installed.cfg 148 | 149 | # Installer logs 150 | pip-log.txt 151 | 152 | # Unit test / coverage reports 153 | .coverage 154 | .tox 155 | 156 | #Translations 157 | *.mo 158 | 159 | #Mr Developer 160 | .mr.developer.cfg 161 | 162 | # Mac crap 163 | .DS_Store 164 | 165 | 166 | ############ 167 | ## Idea 168 | ############ 169 | 170 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | REF, or `r()` is a nicer alternative to PHP's [`print_r`](http://php.net/manual/en/function.print-r.php) / [`var_dump`](http://php.net/manual/en/function.var-dump.php) functions. 2 | 3 | ## [DEMO](http://dev.digitalnature.eu/php-ref/) ## 4 | 5 | ## Requirements ## 6 | 7 | - (server) PHP 5.3+ (5.4+ displays additional info) 8 | - (client) Any browser, except IE 8 and lower of course 9 | 10 | ## Installation using Composer 11 | 12 | Add REF to your `composer.json`: 13 | 14 | ```js 15 | { 16 | "require": { 17 | "digitalnature/php-ref": "dev-master" 18 | } 19 | } 20 | ``` 21 | 22 | Now tell composer to download the bundle by running: 23 | 24 | ``` bash 25 | $ php composer.phar update digitalnature/php-ref 26 | ``` 27 | 28 | Composer will install the bundle to the directory `vendor/digitalnature`. 29 | 30 | ## Usage ## 31 | 32 | Basic example: 33 | 34 | // include the class (not needed if project runs with Composer because it's auto-loaded) 35 | require '/full/path/to/ref.php'; 36 | 37 | // display info about defined classes 38 | r(get_declared_classes()); 39 | 40 | // display info about global variables 41 | r($GLOBALS); 42 | 43 | To print in text mode you can use the `rt()` function instead: 44 | 45 | rt($var); 46 | 47 | To terminate the script after the info is dumped, prepend the bitwise NOT operator: 48 | 49 | ~r($var); // html 50 | ~rt($var); // text 51 | 52 | Prepending the error control operator (@) will return the information: 53 | 54 | $output = @r($var); // html 55 | $output = @rt($var); // text 56 | 57 | Keyboard shortcuts (javascript must be enabled): 58 | 59 | - `X` - collapses / expands all levels 60 | - `Ctrl` + `X` - toggles display state 61 | 62 | To modify the global configuration call `ref::config()`: 63 | 64 | // example: initially expand first 3 levels 65 | ref::config('expLvl', 3); 66 | 67 | You can also add configuration options in your `php.ini` file like this: 68 | 69 | [ref] 70 | ref.expLvl = 3 71 | ref.maxDepth = 4 72 | 73 | Currently available options and their default values: 74 | 75 | | Option | Default | Description 76 | |:------------------------- |:------------------- |:----------------------------------------------- 77 | | `'expLvl'` | `1` | Initially expanded levels (for HTML mode only). A negative value will expand all levels 78 | | `'maxDepth'` | `6` | Maximum depth (`0` to disable); note that disabling it or setting a high value can produce a 100+ MB page when input involves large data 79 | | `'showIteratorContents'` | `FALSE` | Display iterator data (keys and values) 80 | | `'showResourceInfo'` | `TRUE` | Display additional information about resources 81 | | `'showMethods'` | `TRUE` | Display methods and parameter information on objects 82 | | `'showPrivateMembers'` | `FALSE` | Include private properties and methods 83 | | `'showStringMatches'` | `TRUE` | Perform and display string matches for dates, files, json strings, serialized data, regex patterns etc. (SLOW) 84 | | `'formatters'` | `array()` | Custom/external formatters (as associative array: format => className) 85 | | `'shortcutFunc'` | `array('r', 'rt')` | Shortcut functions used to detect the input expression. If they are namespaced, the namespace must be present as well (methods are not supported) 86 | | `'stylePath'` | `'{:dir}/ref.css'` | Local path to a custom stylesheet (HTML only); `FALSE` means that no CSS is included. 87 | | `'scriptPath'` | `'{:dir}/ref.js'` | Local path to a custom javascript (HTML only); `FALSE` means no javascript (tooltips / toggle / kbd shortcuts require JS) 88 | | `'showUrls'` | `FALSE` | Gets information about URLs. Setting to false can improve performance (requires showStringMatches to be TRUE) 89 | | `'timeout'` | `10` | Stop execution after this amount of seconds, forcing an incomplete listing. Applies to all calls 90 | | `'validHtml'` | `FALSE` | For HTML mode only. Whether to produce W3C-valid HTML (larger code output) or unintelligible, potentially browser-incompatible but much smaller code output 91 | 92 | ## Similar projects 93 | 94 | - [Kint](http://raveren.github.io/kint/) 95 | - [dump_r](https://github.com/leeoniya/dump_r.php) 96 | - [Krumo](http://sourceforge.net/projects/krumo/) 97 | - [dBug](http://dbug.ospinto.com/) 98 | - [symfony-vardumper](http://www.sitepoint.com/var_dump-introducing-symfony-vardumper/) 99 | 100 | 101 | ## TODOs 102 | 103 | - Inherit DocBlock comments from parent or prototype, if missing 104 | - Refactor "bubbles" (for text-mode) 105 | - Correctly indent multi-line strings (text-mode) 106 | - Move separator tokens to ::before and ::after pseudo-elements (html-mode) 107 | 108 | 109 | ## License 110 | 111 | http://opensource.org/licenses/mit-license.html 112 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "digitalnature/php-ref", 3 | "description": "A nicer print_r/var_dump alternative for PHP 5.3+", 4 | "keywords": ["debug","var_dump"], 5 | "homepage": "https://github.com/digitalnature/php-ref", 6 | "license": "MIT", 7 | "require": { 8 | "php": ">=5.3.0" 9 | }, 10 | "autoload": { 11 | "files": [ "ref.php" ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ref.css: -------------------------------------------------------------------------------- 1 | .ref{ 2 | font: normal normal 12px/18px Consolas, "Liberation Mono", Menlo, "Courier New", Courier, monospace; 3 | color: #333; 4 | } 5 | 6 | /* reset default styles for these elements */ 7 | .ref i, 8 | .ref span, 9 | .ref r, 10 | .ref a{ 11 | font-style: inherit; 12 | font-weight: inherit; 13 | margin: 0; 14 | padding: 0; 15 | text-align: left; 16 | display: inline; 17 | text-decoration: inherit; 18 | white-space: normal; 19 | background: none; 20 | } 21 | 22 | /* meta content (used to generate tooltips) */ 23 | .ref > div, 24 | .ref > t{ 25 | display: none; 26 | } 27 | 28 | /* show help cursor when mouse is over an entity with a tooltip */ 29 | .ref [data-tip], 30 | .ref [h]{ 31 | cursor: help; 32 | } 33 | 34 | /* pointer if inside a link */ 35 | .ref a > [data-tip], 36 | .ref a > [h]{ 37 | cursor: pointer; 38 | } 39 | 40 | /* links */ 41 | .ref a{ 42 | color: inherit; 43 | border-bottom: 1px dotted transparent; 44 | border-color: inherit; 45 | } 46 | 47 | /* tooltip; note that the js overrides top/left properties, this is why we use margin */ 48 | #rTip{ 49 | display: none; 50 | position: absolute; 51 | z-index: 99999; 52 | font-size: 12px; 53 | white-space: pre; 54 | text-align: left; 55 | text-shadow: 0 -1px 0 #191919; 56 | line-height: 16px; 57 | background: #222; 58 | color: #888; 59 | border: 0; 60 | border-radius: 4px; 61 | opacity: 0.90; 62 | box-shadow:0 0 4px rgba(0,0,0, 0.25); 63 | -webkit-transition: opacity .25s, margin .25s; 64 | transition: opacity .25s, margin .25s; 65 | } 66 | 67 | #rTip.visible{ 68 | display: table; 69 | margin: 10px 0 0 15px; 70 | } 71 | 72 | #rTip.visible.fadingOut{ 73 | opacity: 0; 74 | margin: 20px 0 0 25px; 75 | } 76 | 77 | #rTip [data-cell], 78 | #rTip [c]{ 79 | padding: 2px 7px; 80 | } 81 | 82 | #rTip [data-title], #rTip [data-desc]{ 83 | padding: 8px; 84 | display: block; 85 | color: #ccc; 86 | } 87 | 88 | #rTip [data-desc]{ 89 | padding-top: 0px; 90 | color: #777; 91 | } 92 | 93 | #rTip [data-cell][data-varType], 94 | #rTip [c][data-varType]{ 95 | padding: 10px; 96 | background: #333; 97 | box-shadow: inset -1px 0 0 #444; 98 | border-right:1px solid #111; 99 | border-top-left-radius: 4px; 100 | border-bottom-left-radius: 4px; 101 | } 102 | 103 | #rTip [data-cell][data-sub], 104 | #rTip [c][data-sub]{ 105 | padding: 8px 10px 10px 10px; 106 | background: #333; 107 | box-shadow: inset 0 1px 0 #444; 108 | border-top:1px solid #111; 109 | border-bottom-right-radius: 4px; 110 | border-bottom-left-radius: 4px; 111 | } 112 | 113 | #rTip [data-table] [data-cell]:first-child, 114 | #rTip [t] [c]:first-child{ 115 | font: bold 11px Helvetica, Arial; 116 | color: #888; 117 | } 118 | 119 | #rTip [data-table] [data-cell]:nth-child(2), 120 | #rTip [t] [c]:nth-child(2){ 121 | color: #edd078; 122 | } 123 | 124 | 125 | 126 | 127 | /* base entity - can be nested */ 128 | .ref span, .ref r{ 129 | white-space: pre; 130 | display: inline; 131 | } 132 | 133 | /* key-value dividers, property & method modifiers etc. */ 134 | .ref i{ 135 | white-space: pre; 136 | color: #aaa; 137 | } 138 | 139 | /* source expression (input) */ 140 | .ref [data-input]{ 141 | margin: 2px 0 0; 142 | padding: 2px 7px 3px 4px; 143 | display: block; 144 | color: #ccc; 145 | background-color: #333; 146 | background-image: -webkit-linear-gradient(top, #444, #333); 147 | background-image: linear-gradient(top, #444, #333); 148 | border-radius: 4px 4px 0 0; 149 | border-bottom: 1px solid #fff; 150 | } 151 | 152 | .ref [data-backtrace]{ 153 | float: right; 154 | } 155 | 156 | .ref [data-output]{ 157 | background: #f9f9f9; 158 | border: 1px solid #eee; 159 | border-top: 0; 160 | border-radius: 0 0 4px 4px; 161 | box-shadow: inset 0px 4px 4px #f3f3f3, inset 0px -8px 8px #fff; 162 | padding: 2px 5px; 163 | margin: 0 0 4px; 164 | text-shadow: 0 1px 0 #fff; 165 | display: block; 166 | } 167 | 168 | /* expand/collapse toggle link for groups */ 169 | .ref [data-toggle]{ 170 | display: inline-block; 171 | vertical-align: -3px; 172 | margin-left: 2px; 173 | width: 0px; 174 | height: 0px; 175 | border-style: solid; 176 | border-width: 7px 0 7px 10px; 177 | border-color: transparent transparent transparent #CC0033; 178 | cursor: pointer; 179 | -webkit-transition: all ease-in .15s; 180 | transition: all ease-in .15s; 181 | } 182 | 183 | /* collapse graphic */ 184 | .ref [data-toggle][data-exp]{ 185 | -webkit-transform: rotate(90deg); 186 | -ms-transform: rotate(90deg); 187 | transform: rotate(90deg); 188 | } 189 | 190 | .ref [data-group], 191 | .ref [g]{ 192 | display: none; 193 | } 194 | 195 | .ref [data-toggle][data-exp] ~ [data-group], 196 | .ref [data-toggle][data-exp] ~ [g]{ 197 | display: block; 198 | } 199 | 200 | /* group sections */ 201 | .ref [data-table], 202 | .ref [t]{ 203 | display: table; 204 | } 205 | 206 | /* section titles */ 207 | .ref [data-tHead]{ 208 | font: bold 11px Helvetica, Arial; 209 | color: #bcbcbc; 210 | text-transform: lowercase; 211 | margin: 12px 0 2px 10px; 212 | display: block; 213 | } 214 | 215 | /* emulate a table for displaying array & object members */ 216 | /* section row */ 217 | .ref [data-row], 218 | .ref [r]{ 219 | display: table-row; 220 | } 221 | 222 | /* zebra-like rows */ 223 | .ref [data-output] [data-row]:nth-child(odd){background: #f4f4f4;} 224 | .ref [data-output] [data-row]:nth-child(even){background: #f9f9f9;} 225 | .ref [data-output] [r]:nth-child(odd){background: #f4f4f4;} 226 | .ref [data-output] [r]:nth-child(even){background: #f9f9f9;} 227 | 228 | /* section cells */ 229 | .ref [data-cell], 230 | .ref [c]{ 231 | display: table-cell; 232 | width: auto; 233 | vertical-align: top; 234 | padding: 1px 0 1px 10px; 235 | } 236 | 237 | /* last cell of a row (forces table to adjust width like we want to) */ 238 | .ref [data-output] [data-table], 239 | .ref [data-output] [t], 240 | .ref [data-output] [data-cell]:last-child, 241 | .ref [data-output] [c]:last-child{ 242 | width: 100%; 243 | } 244 | 245 | 246 | 247 | /* tag-like appearance for boolean, null and resource types */ 248 | .ref [data-true], 249 | .ref [data-false], 250 | .ref [data-null], 251 | .ref [data-unknown], 252 | .ref [data-resource], 253 | .ref [data-match], 254 | .ref [m]{ 255 | font: bold 11px Helvetica, Arial; 256 | color: #fff; 257 | padding: 1px 3px; 258 | text-transform: lowercase; 259 | text-shadow: none; 260 | border-radius: 2px; 261 | margin-right: 5px; 262 | background-color: #eee; 263 | background-image: -webkit-linear-gradient(top, rgba(255,255,255,0.1) 40%,rgba(0,0,0,0.1) 100%); 264 | background-image: linear-gradient(to bottom, rgba(255,255,255,0.1) 40%,rgba(0,0,0,0.1) 100%); 265 | } 266 | 267 | /* string matches */ 268 | .ref [data-match], 269 | .ref [m]{ 270 | background-color: #d78035; 271 | } 272 | 273 | /* boolean true */ 274 | .ref [data-true]{ 275 | background-color: #339900; 276 | } 277 | 278 | /* boolean false */ 279 | .ref [data-false]{ 280 | background-color: #CC0033; 281 | color: #fff; 282 | } 283 | 284 | /* null value */ 285 | .ref [data-null], 286 | .ref [data-unknown]{ 287 | background-color: #eee; 288 | color: #999; 289 | text-shadow: inherit; 290 | } 291 | 292 | /* resources */ 293 | .ref [data-resource]{ 294 | background-color: #0057ae; 295 | } 296 | 297 | .ref [data-resourceProp]{ 298 | font: bold 11px Helvetica, Arial; 299 | color: #999; 300 | } 301 | 302 | /* integer or double values */ 303 | .ref [data-integer], 304 | .ref [data-double]{ 305 | color: #0099CC; 306 | } 307 | 308 | /* string values */ 309 | .ref [data-string]{ 310 | background: #e8f0e1; 311 | color: #669933; 312 | padding: 3px 1px; 313 | 314 | /* prevent long strings from breaking the page layout */ 315 | white-space: -moz-pre-wrap; /* Mozilla */ 316 | white-space: -hp-pre-wrap; /* HP printers */ 317 | white-space: -o-pre-wrap; /* Opera 7 */ 318 | white-space: -pre-wrap; /* Opera 4-6 */ 319 | white-space: pre-wrap; /* CSS 2.1 */ 320 | word-wrap: break-word; /* IE */ 321 | word-break: break-all; 322 | } 323 | 324 | .ref [data-string][data-special]{ 325 | background: none; 326 | padding: 0; 327 | } 328 | 329 | .ref [data-string][data-special] i{ 330 | background: #faf3dc; 331 | color: #d78035; 332 | } 333 | 334 | /* arrays & objects */ 335 | .ref [data-array], 336 | .ref [data-array] ~ i, 337 | .ref [data-object], 338 | .ref [data-object] ~ i, 339 | .ref [data-resource] ~ i{ 340 | color:#CC0033; 341 | } 342 | 343 | .ref [data-method]{ 344 | font-weight: bold; 345 | color: #0057ae; 346 | } 347 | 348 | .ref [data-const][data-inherited], 349 | .ref [data-prop][data-inherited]{ 350 | color: #999; 351 | } 352 | 353 | .ref [data-prop][data-private], 354 | .ref [data-method][data-private]{ 355 | color: #CC0033; 356 | } 357 | 358 | /* inherited methods */ 359 | .ref [data-method][data-inherited]{ 360 | font-weight: bold; 361 | color: #6da5de; 362 | } 363 | 364 | /* method arguments */ 365 | .ref [data-param]{ 366 | font-weight: normal; 367 | color: #333; 368 | } 369 | 370 | /* optional method arguments */ 371 | .ref [data-param][data-optional]{ 372 | font-style: italic; 373 | font-weight: normal; 374 | color: #aaa; 375 | } 376 | 377 | /* group info prefix */ 378 | .ref [data-gLabel], 379 | .ref [gl]{ 380 | font: bold 11px Helvetica, Arial; 381 | padding: 0 3px; 382 | color: #333; 383 | } 384 | 385 | /* tiny bubbles that indicate visibility info or class features */ 386 | .ref [data-mod]{ 387 | font: bold 11px Helvetica, Arial; 388 | text-shadow: none; 389 | color: #fff; 390 | } 391 | 392 | .ref [data-input] [data-mod]{ 393 | color: #444; 394 | } 395 | 396 | .ref [data-mod] span, 397 | .ref [data-mod] r{ 398 | display: inline-block; 399 | margin: 0 2px; 400 | width: 14px; 401 | height: 14px; 402 | text-align: center; 403 | border-radius: 30px; 404 | line-height: 15px; 405 | } 406 | 407 | .ref [data-mod-interface], 408 | .ref [data-mod-abstract]{ 409 | background: #baed78; 410 | } 411 | 412 | .ref [data-mod-anonymous]{ 413 | background: #444; 414 | } 415 | 416 | .ref [data-mod-protected]{ 417 | background: #edd078; 418 | } 419 | 420 | .ref [data-mod-private]{ 421 | background: #eea8b9; 422 | } 423 | 424 | .ref [data-mod-iterateable]{ 425 | background: #d5dea5; 426 | } 427 | 428 | .ref [data-mod-cloneable]{ 429 | background: #bdd7d1; 430 | } 431 | 432 | .ref [data-mod-final]{ 433 | background: #78bded; 434 | } 435 | 436 | /* regular expression (colors partially match RegexBuddy and RegexPal) */ 437 | .ref [data-regex]{ 438 | font-weight: bold; 439 | text-shadow: none; 440 | padding: 1px 0; 441 | background: #e6e6e6; 442 | word-wrap: break-word; 443 | } 444 | 445 | /* char class */ 446 | .ref [data-regex-chr]{ 447 | background: #ffc080; 448 | color: #694c07; 449 | } 450 | 451 | .ref [data-regex-chr-meta]{background: #e0a060;} /* char class: metasequence */ 452 | .ref [data-regex-chr-range]{background: #ffcf9b;} /* char class: range-hyphen */ 453 | 454 | /* metasequence */ 455 | .ref [data-regex-meta]{ 456 | background: #80c0ff; 457 | color: #105f8c; 458 | } 459 | 460 | /* group: depth 1 */ 461 | .ref [data-regex-g1]{ 462 | background: #00c000; 463 | color: #fff; 464 | } 465 | 466 | /* group: depth 2 */ 467 | .ref [data-regex-g2]{ 468 | background: #c3e86c; 469 | color: #648c1c; 470 | } 471 | 472 | /* group: depth 3 */ 473 | .ref [data-regex-g3]{ 474 | background: #008000; 475 | color: #fff; 476 | } 477 | 478 | /* group: depth 4 */ 479 | .ref [data-regex-g4]{ 480 | background: #6dcb99; 481 | color: #fff; 482 | } 483 | 484 | /* group: depth 5 */ 485 | .ref [data-regex-g5]{ 486 | background: #00ff00; 487 | color: #2c8e24; 488 | } 489 | 490 | .ref [data-error]{ 491 | background: #CC0033; 492 | color: #fff; 493 | border-radius: 0 0 4px 4px; 494 | padding: 2px 5px; 495 | margin: 0 0 4px; 496 | display: block; 497 | } 498 | 499 | /* make labels and less-relevant text non-selectable */ 500 | .ref [data-match], 501 | .ref [m], 502 | .ref [data-tHead], 503 | .ref [data-gLabel], 504 | .ref [gl], 505 | .ref [data-mod]{ 506 | -webkit-user-select: none; 507 | -moz-user-select: none; 508 | -ms-user-select: none; 509 | user-select: none; 510 | } 511 | -------------------------------------------------------------------------------- /ref.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', function(){ 2 | 3 | var tip = document.createElement('div'), 4 | refs = document.querySelectorAll('.ref'); 5 | 6 | for(var i = 0, m = refs.length; i < m; i++){ 7 | var kbds = refs[i].querySelectorAll('[data-toggle]'), 8 | tippable = refs[i].querySelectorAll('[data-tip],[h]'), 9 | tips = refs[i].querySelectorAll('div, t'); 10 | 11 | for(var j = 0, n = kbds.length; j < n; j++){ 12 | if(kbds[j].parentNode !== refs[i]) 13 | kbds[j].onclick = function(e){ 14 | ('exp' in this.dataset) ? delete this.dataset.exp : this.dataset.exp = 1; 15 | } 16 | } 17 | 18 | [].filter.call(tips, function(node){ 19 | return node.parentNode == refs[i]; 20 | }); 21 | 22 | for(var j = 0, n = tippable.length; j < n; j++){ 23 | tippable[j].tipRef = tips[tippable[j].dataset.tip] || tips[tippable[j].getAttribute('h')]; 24 | tippable[j].onmouseover = function(){ 25 | tip.className = 'ref visible'; 26 | tip.innerHTML = this.tipRef.innerHTML; 27 | window.clearTimeout(tip.fadeOut); 28 | }; 29 | tippable[j].onmouseout = function(){ 30 | tip.className = 'ref visible fadingOut'; 31 | tip.fadeOut = window.setTimeout(function(){ 32 | tip.innerHTML = ''; 33 | tip.className = ''; 34 | }, 250); 35 | }; 36 | } 37 | 38 | refs[i].onmousemove = function(e){ 39 | if(tip.className.indexOf('visible') < 0) 40 | return; 41 | tip.style.top = ((document.documentElement.clientHeight - e.clientY) < tip.offsetHeight + 20 ? Math.max(e.pageY - tip.offsetHeight, 0) : e.pageY) + 'px'; 42 | tip.style.left = ((document.documentElement.clientWidth - e.clientX) < tip.offsetWidth + 20 ? Math.max(e.pageX - tip.offsetWidth, 0) : e.pageX) + 'px'; 43 | }; 44 | } 45 | 46 | tip.id = 'rTip'; 47 | document.body.appendChild(tip); 48 | 49 | window.addEventListener('keydown', function(e){ 50 | if((e.keyCode != 88) || (['input', 'textarea', 'select'].indexOf(e.target.tagName.toLowerCase()) > -1)) 51 | return; 52 | 53 | e.preventDefault(); 54 | 55 | if(e.ctrlKey && e.keyCode == 88){ 56 | var d = refs[0].style.display !== 'none' ? 'none' : 'block'; 57 | for(var i = 0, n = refs.length; i < n; i++) 58 | refs[i].style.display = d; 59 | 60 | return; 61 | } 62 | 63 | var kbds = document.querySelectorAll('.ref [data-toggle]'), 64 | m = kbds.length, 65 | partlyExp = document.querySelectorAll('.ref [data-toggle][data-exp]').length !== m; 66 | 67 | for(var i = 0; i < m; i++) 68 | partlyExp ? (kbds[i].dataset.exp = 1) : (delete kbds[i].dataset.exp); 69 | 70 | }); 71 | 72 | }); 73 | 74 | -------------------------------------------------------------------------------- /ref.php: -------------------------------------------------------------------------------- 1 | REF'; 33 | 34 | $ref = new ref($format); 35 | 36 | if($capture) 37 | ob_start(); 38 | 39 | foreach($args as $index => $arg) 40 | $ref->query($arg, $expressions ? $expressions[$index] : null); 41 | 42 | // return the results if this function was called with the error suppression operator 43 | if($capture) 44 | return ob_get_clean(); 45 | 46 | // stop the script if this function was called with the bitwise not operator 47 | if(in_array('~', $options, true) && ($format === 'html')){ 48 | print ''; 49 | exit(0); 50 | } 51 | } 52 | 53 | 54 | 55 | /** 56 | * Shortcut to ref, plain text mode 57 | * 58 | * @param mixed $args 59 | * @return void|string 60 | */ 61 | function rt(){ 62 | $args = func_get_args(); 63 | $options = array(); 64 | $output = ''; 65 | $expressions = ref::getInputExpressions($options); 66 | $capture = in_array('@', $options, true); 67 | $ref = new ref((php_sapi_name() !== 'cli') || $capture ? 'text' : 'cliText'); 68 | 69 | if(func_num_args() !== count($expressions)) 70 | $expressions = null; 71 | 72 | if(!headers_sent()) 73 | header('Content-Type: text/plain; charset=utf-8'); 74 | 75 | if($capture) 76 | ob_start(); 77 | 78 | foreach($args as $index => $arg) 79 | $ref->query($arg, $expressions ? $expressions[$index] : null); 80 | 81 | if($capture) 82 | return ob_get_clean(); 83 | 84 | if(in_array('~', $options, true)) 85 | exit(0); 86 | } 87 | 88 | 89 | 90 | /** 91 | * REF is a nicer alternative to PHP's print_r() / var_dump(). 92 | * 93 | * @version 1.0 94 | * @author digitalnature - http://digitalnature.eu 95 | */ 96 | class ref{ 97 | 98 | const 99 | 100 | MARKER_KEY = '_phpRefArrayMarker_'; 101 | 102 | 103 | 104 | protected static 105 | 106 | /** 107 | * CPU time used for processing 108 | * 109 | * @var array 110 | */ 111 | $time = 0, 112 | 113 | /** 114 | * Configuration (+ default values) 115 | * 116 | * @var array 117 | */ 118 | $config = array( 119 | 120 | // initially expanded levels (for HTML mode only) 121 | 'expLvl' => 1, 122 | 123 | // depth limit (0 = no limit); 124 | // this is not related to recursion 125 | 'maxDepth' => 6, 126 | 127 | // show the place where r() has been called from 128 | 'showBacktrace' => true, 129 | 130 | // display iterator contents 131 | 'showIteratorContents' => false, 132 | 133 | // display extra information about resources 134 | 'showResourceInfo' => true, 135 | 136 | // display method and parameter list on objects 137 | 'showMethods' => true, 138 | 139 | // display private properties / methods 140 | 'showPrivateMembers' => false, 141 | 142 | // peform string matches (date, file, functions, classes, json, serialized data, regex etc.) 143 | // note: seriously slows down queries on large amounts of data 144 | 'showStringMatches' => true, 145 | 146 | // shortcut functions used to access the query method below; 147 | // if they are namespaced, the namespace must be present as well (methods are not supported) 148 | 'shortcutFunc' => array('r', 'rt'), 149 | 150 | // custom/external formatters (as associative array: format => className) 151 | 'formatters' => array(), 152 | 153 | // stylesheet path (for HTML only); 154 | // 'false' means no styles 155 | 'stylePath' => '{:dir}/ref.css', 156 | 157 | // javascript path (for HTML only); 158 | // 'false' means no js 159 | 'scriptPath' => '{:dir}/ref.js', 160 | 161 | // display url info via cURL 162 | 'showUrls' => false, 163 | 164 | // stop evaluation after this amount of time (seconds) 165 | 'timeout' => 10, 166 | 167 | // whether to produce W3c-valid HTML, 168 | // or unintelligible, but optimized markup that takes less space 169 | 'validHtml' => false, 170 | ), 171 | 172 | /** 173 | * Some environment variables 174 | * used to determine feature support 175 | * 176 | * @var array 177 | */ 178 | $env = array(), 179 | 180 | /** 181 | * Timeout point 182 | * 183 | * @var bool 184 | */ 185 | $timeout = -1, 186 | 187 | $debug = array( 188 | 'cacheHits' => 0, 189 | 'objects' => 0, 190 | 'arrays' => 0, 191 | 'scalars' => 0, 192 | ); 193 | 194 | 195 | protected 196 | 197 | /** 198 | * Output formatter of this instance 199 | * 200 | * @var RFormatter 201 | */ 202 | $fmt = null, 203 | 204 | /** 205 | * Start time of the current instance 206 | * 207 | * @var float 208 | */ 209 | $startTime = 0, 210 | 211 | /** 212 | * Internally created objects 213 | * 214 | * @var SplObjectStorage 215 | */ 216 | $intObjects = null; 217 | 218 | 219 | 220 | /** 221 | * Constructor 222 | * 223 | * @param string|RFormatter $format Output format ID, or formatter instance defaults to 'html' 224 | */ 225 | public function __construct($format = 'html'){ 226 | 227 | static $didIni = false; 228 | 229 | if(!$didIni){ 230 | $didIni = true; 231 | foreach(array_keys(static::$config) as $key){ 232 | $iniVal = get_cfg_var('ref.' . $key); 233 | if($iniVal !== false) 234 | static::$config[$key] = $iniVal; 235 | } 236 | 237 | } 238 | 239 | if($format instanceof RFormatter){ 240 | $this->fmt = $format; 241 | 242 | }else{ 243 | $format = isset(static::$config['formatters'][$format]) ? static::$config['formatters'][$format] : 'R' . ucfirst($format) . 'Formatter'; 244 | 245 | if(!class_exists($format, false)) 246 | throw new \Exception(sprintf('%s class not found', $format)); 247 | 248 | $this->fmt = new $format(); 249 | } 250 | 251 | if(static::$env) 252 | return; 253 | 254 | static::$env = array( 255 | 256 | // php 5.4+ ? 257 | 'is54' => version_compare(PHP_VERSION, '5.4') >= 0, 258 | 259 | // php 5.4.6+ ? 260 | 'is546' => version_compare(PHP_VERSION, '5.4.6') >= 0, 261 | 262 | // php 5.6+ 263 | 'is56' => version_compare(PHP_VERSION, '5.6') >= 0, 264 | 265 | // php 7.0+ ? 266 | 'is7' => version_compare(PHP_VERSION, '7.0') >= 0, 267 | 268 | // curl extension running? 269 | 'curlActive' => function_exists('curl_version'), 270 | 271 | // is the 'mbstring' extension active? 272 | 'mbStr' => function_exists('mb_detect_encoding'), 273 | 274 | // @see: https://bugs.php.net/bug.php?id=52469 275 | 'supportsDate' => (strncasecmp(PHP_OS, 'WIN', 3) !== 0) || (version_compare(PHP_VERSION, '5.3.10') >= 0), 276 | ); 277 | } 278 | 279 | 280 | 281 | /** 282 | * Enforce proper use of this class 283 | * 284 | * @param string $name 285 | */ 286 | public function __get($name){ 287 | throw new \Exception(sprintf('No such property: %s', $name)); 288 | } 289 | 290 | 291 | 292 | /** 293 | * Enforce proper use of this class 294 | * 295 | * @param string $name 296 | * @param mixed $value 297 | */ 298 | public function __set($name, $value){ 299 | throw new \Exception(sprintf('Cannot set %s. Not allowed', $name)); 300 | } 301 | 302 | 303 | 304 | /** 305 | * Generate structured information about a variable/value/expression (subject) 306 | * 307 | * Output is flushed to the screen 308 | * 309 | * @param mixed $subject 310 | * @param string $expression 311 | */ 312 | public function query($subject, $expression = null){ 313 | 314 | if(static::$timeout > 0) 315 | return; 316 | 317 | $this->startTime = microtime(true); 318 | 319 | $this->intObjects = new \SplObjectStorage(); 320 | 321 | $this->fmt->startRoot(); 322 | $this->fmt->startExp(); 323 | $this->evaluateExp($expression); 324 | $this->fmt->endExp(); 325 | $this->evaluate($subject); 326 | $this->fmt->endRoot(); 327 | $this->fmt->flush(); 328 | 329 | static::$time += microtime(true) - $this->startTime; 330 | } 331 | 332 | 333 | 334 | 335 | /** 336 | * Executes a function the given number of times and returns the elapsed time. 337 | * 338 | * Keep in mind that the returned time includes function call overhead (including 339 | * microtime calls) x iteration count. This is why this is better suited for 340 | * determining which of two or more functions is the fastest, rather than 341 | * finding out how fast is a single function. 342 | * 343 | * @param int $iterations Number of times the function will be executed 344 | * @param callable $function Function to execute 345 | * @param mixed &$output If given, last return value will be available in this variable 346 | * @return double Elapsed time 347 | */ 348 | public static function timeFunc($iterations, $function, &$output = null){ 349 | 350 | $time = 0; 351 | 352 | for($i = 0; $i < $iterations; $i++){ 353 | $start = microtime(true); 354 | $output = call_user_func($function); 355 | $time += microtime(true) - $start; 356 | } 357 | 358 | return round($time, 4); 359 | } 360 | 361 | 362 | 363 | /** 364 | * Timer utility 365 | * 366 | * First call of this function will start the timer. 367 | * The second call will stop the timer and return the elapsed time 368 | * since the timer started. 369 | * 370 | * Multiple timers can be controlled simultaneously by specifying a timer ID. 371 | * 372 | * @since 1.0 373 | * @param int $id Timer ID, optional 374 | * @param int $precision Precision of the result, optional 375 | * @return void|double Elapsed time, or void if the timer was just started 376 | */ 377 | public static function timer($id = 1, $precision = 4){ 378 | 379 | static 380 | $timers = array(); 381 | 382 | // check if this timer was started, and display the elapsed time if so 383 | if(isset($timers[$id])){ 384 | $elapsed = round(microtime(true) - $timers[$id], $precision); 385 | unset($timers[$id]); 386 | return $elapsed; 387 | } 388 | 389 | // ID doesn't exist, start new timer 390 | $timers[$id] = microtime(true); 391 | } 392 | 393 | 394 | 395 | /** 396 | * Parses a DocBlock comment into a data structure. 397 | * 398 | * @link http://pear.php.net/manual/en/standards.sample.php 399 | * @param string $comment DocBlock comment (must start with /**) 400 | * @param string|null $key Field to return (optional) 401 | * @return array|string|null Array containing all fields, array/string with the contents of 402 | * the requested field, or null if the comment is empty/invalid 403 | */ 404 | public static function parseComment($comment, $key = null){ 405 | 406 | $description = ''; 407 | $tags = array(); 408 | $tag = null; 409 | $pointer = ''; 410 | $padding = 0; 411 | $comment = preg_split('/\r\n|\r|\n/', '* ' . trim($comment, "/* \t\n\r\0\x0B")); 412 | 413 | // analyze each line 414 | foreach($comment as $line){ 415 | 416 | // drop any wrapping spaces 417 | $line = trim($line); 418 | 419 | // drop "* " 420 | if($line !== '') 421 | $line = substr($line, 2); 422 | 423 | if(strpos($line, '@') !== 0){ 424 | 425 | // preserve formatting of tag descriptions, 426 | // because they may span across multiple lines 427 | if($tag !== null){ 428 | $trimmed = trim($line); 429 | 430 | if($padding !== 0) 431 | $trimmed = static::strPad($trimmed, static::strLen($line) - $padding, ' ', STR_PAD_LEFT); 432 | else 433 | $padding = static::strLen($line) - static::strLen($trimmed); 434 | 435 | $pointer .= "\n{$trimmed}"; 436 | continue; 437 | } 438 | 439 | // tag definitions have not started yet; assume this is part of the description text 440 | $description .= "\n{$line}"; 441 | continue; 442 | } 443 | 444 | $padding = 0; 445 | $parts = explode(' ', $line, 2); 446 | 447 | // invalid tag? (should we include it as an empty array?) 448 | if(!isset($parts[1])) 449 | continue; 450 | 451 | $tag = substr($parts[0], 1); 452 | $line = ltrim($parts[1]); 453 | 454 | // tags that have a single component (eg. link, license, author, throws...); 455 | // note that @throws may have 2 components, however most people use it like "@throws ExceptionClass if whatever...", 456 | // which, if broken into two values, leads to an inconsistent description sentence 457 | if(!in_array($tag, array('global', 'param', 'return', 'var'))){ 458 | $tags[$tag][] = $line; 459 | end($tags[$tag]); 460 | $pointer = &$tags[$tag][key($tags[$tag])]; 461 | continue; 462 | } 463 | 464 | // tags with 2 or 3 components (var, param, return); 465 | $parts = explode(' ', $line, 2); 466 | $parts[1] = isset($parts[1]) ? ltrim($parts[1]) : null; 467 | $lastIdx = 1; 468 | 469 | // expecting 3 components on the 'param' tag: type varName varDescription 470 | if($tag === 'param'){ 471 | $lastIdx = 2; 472 | if(in_array($parts[1][0], array('&', '$'), true)){ 473 | $line = ltrim(array_pop($parts)); 474 | $parts = array_merge($parts, explode(' ', $line, 2)); 475 | $parts[2] = isset($parts[2]) ? ltrim($parts[2]) : null; 476 | }else{ 477 | $parts[2] = $parts[1]; 478 | $parts[1] = null; 479 | } 480 | } 481 | 482 | $tags[$tag][] = $parts; 483 | end($tags[$tag]); 484 | $pointer = &$tags[$tag][key($tags[$tag])][$lastIdx]; 485 | } 486 | 487 | // split title from the description texts at the nearest 2x new-line combination 488 | // (note: loose check because 0 isn't valid as well) 489 | if(strpos($description, "\n\n")){ 490 | list($title, $description) = explode("\n\n", $description, 2); 491 | 492 | // if we don't have 2 new lines, try to extract first sentence 493 | }else{ 494 | // in order for a sentence to be considered valid, 495 | // the next one must start with an uppercase letter 496 | $sentences = preg_split('/(?<=[.?!])\s+(?=[A-Z])/', $description, 2, PREG_SPLIT_NO_EMPTY); 497 | 498 | // failed to detect a second sentence? then assume there's only title and no description text 499 | $title = isset($sentences[0]) ? $sentences[0] : $description; 500 | $description = isset($sentences[1]) ? $sentences[1] : ''; 501 | } 502 | 503 | $title = ltrim($title); 504 | $description = ltrim($description); 505 | 506 | $data = compact('title', 'description', 'tags'); 507 | 508 | if(!array_filter($data)) 509 | return null; 510 | 511 | if($key !== null) 512 | return isset($data[$key]) ? $data[$key] : null; 513 | 514 | return $data; 515 | } 516 | 517 | 518 | 519 | /** 520 | * Split a regex into its components 521 | * 522 | * Based on "Regex Colorizer" by Steven Levithan (this is a translation from javascript) 523 | * 524 | * @link https://github.com/slevithan/regex-colorizer 525 | * @link https://github.com/symfony/Finder/blob/master/Expression/Regex.php#L64-74 526 | * @param string $pattern 527 | * @return array 528 | */ 529 | public static function splitRegex($pattern){ 530 | 531 | // detection attempt code from the Symfony Finder component 532 | $maybeValid = false; 533 | if(preg_match('/^(.{3,}?)([imsxuADU]*)$/', $pattern, $m)) { 534 | $start = substr($m[1], 0, 1); 535 | $end = substr($m[1], -1); 536 | 537 | if(($start === $end && !preg_match('/[*?[:alnum:] \\\\]/', $start)) || ($start === '{' && $end === '}')) 538 | $maybeValid = true; 539 | } 540 | 541 | if(!$maybeValid) 542 | throw new \Exception('Pattern does not appear to be a valid PHP regex'); 543 | 544 | $output = array(); 545 | $capturingGroupCount = 0; 546 | $groupStyleDepth = 0; 547 | $openGroups = array(); 548 | $lastIsQuant = false; 549 | $lastType = 1; // 1 = none; 2 = alternator 550 | $lastStyle = null; 551 | 552 | preg_match_all('/\[\^?]?(?:[^\\\\\]]+|\\\\[\S\s]?)*]?|\\\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9][0-9]*|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\S\s]?)|\((?:\?[:=!]?)?|(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??|[^.?*+^${[()|\\\\]+|./', $pattern, $matches); 553 | 554 | $matches = $matches[0]; 555 | 556 | $getTokenCharCode = function($token){ 557 | if(strlen($token) > 1 && $token[0] === '\\'){ 558 | $t1 = substr($token, 1); 559 | 560 | if(preg_match('/^c[A-Za-z]$/', $t1)) 561 | return strpos("ABCDEFGHIJKLMNOPQRSTUVWXYZ", strtoupper($t1[1])) + 1; 562 | 563 | if(preg_match('/^(?:x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})$/', $t1)) 564 | return intval(substr($t1, 1), 16); 565 | 566 | if(preg_match('/^(?:[0-3][0-7]{0,2}|[4-7][0-7]?)$/', $t1)) 567 | return intval($t1, 8); 568 | 569 | $len = strlen($t1); 570 | 571 | if($len === 1 && strpos('cuxDdSsWw', $t1) !== false) 572 | return null; 573 | 574 | if($len === 1){ 575 | switch ($t1) { 576 | case 'b': return 8; 577 | case 'f': return 12; 578 | case 'n': return 10; 579 | case 'r': return 13; 580 | case 't': return 9; 581 | case 'v': return 11; 582 | default: return $t1[0]; 583 | } 584 | } 585 | } 586 | 587 | return ($token !== '\\') ? $token[0] : null; 588 | }; 589 | 590 | foreach($matches as $m){ 591 | 592 | if($m[0] === '['){ 593 | $lastCC = null; 594 | $cLastRangeable = false; 595 | $cLastType = 0; // 0 = none; 1 = range hyphen; 2 = short class 596 | 597 | preg_match('/^(\[\^?)(]?(?:[^\\\\\]]+|\\\\[\S\s]?)*)(]?)$/', $m, $parts); 598 | 599 | array_shift($parts); 600 | list($opening, $content, $closing) = $parts; 601 | 602 | if(!$closing) 603 | throw new \Exception('Unclosed character class'); 604 | 605 | preg_match_all('/[^\\\\-]+|-|\\\\(?:[0-3][0-7]{0,2}|[4-7][0-7]?|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\S\s]?)/', $content, $ccTokens); 606 | $ccTokens = $ccTokens[0]; 607 | $ccTokenCount = count($ccTokens); 608 | $output[] = array('chr' => $opening); 609 | 610 | foreach($ccTokens as $i => $cm) { 611 | 612 | if($cm[0] === '\\'){ 613 | if(preg_match('/^\\\\[cux]$/', $cm)) 614 | throw new \Exception('Incomplete regex token'); 615 | 616 | if(preg_match('/^\\\\[dsw]$/i', $cm)) { 617 | $output[] = array('chr-meta' => $cm); 618 | $cLastRangeable = ($cLastType !== 1); 619 | $cLastType = 2; 620 | 621 | }elseif($cm === '\\'){ 622 | throw new \Exception('Incomplete regex token'); 623 | 624 | }else{ 625 | $output[] = array('chr-meta' => $cm); 626 | $cLastRangeable = $cLastType !== 1; 627 | $lastCC = $getTokenCharCode($cm); 628 | } 629 | 630 | }elseif($cm === '-'){ 631 | if($cLastRangeable){ 632 | $nextToken = ($i + 1 < $ccTokenCount) ? $ccTokens[$i + 1] : false; 633 | 634 | if($nextToken){ 635 | $nextTokenCharCode = $getTokenCharCode($nextToken[0]); 636 | 637 | if((!is_null($nextTokenCharCode) && $lastCC > $nextTokenCharCode) || $cLastType === 2 || preg_match('/^\\\\[dsw]$/i', $nextToken[0])) 638 | throw new \Exception('Reversed or invalid range'); 639 | 640 | $output[] = array('chr-range' => '-'); 641 | $cLastRangeable = false; 642 | $cLastType = 1; 643 | 644 | }else{ 645 | $output[] = $closing ? array('chr' => '-') : array('chr-range' => '-'); 646 | } 647 | 648 | }else{ 649 | $output[] = array('chr' => '-'); 650 | $cLastRangeable = ($cLastType !== 1); 651 | } 652 | 653 | }else{ 654 | $output[] = array('chr' => $cm); 655 | $cLastRangeable = strlen($cm) > 1 || ($cLastType !== 1); 656 | $lastCC = $cm[strlen($cm) - 1]; 657 | } 658 | } 659 | 660 | $output[] = array('chr' => $closing); 661 | $lastIsQuant = true; 662 | 663 | }elseif($m[0] === '('){ 664 | if(strlen($m) === 2) 665 | throw new \Exception('Invalid or unsupported group type'); 666 | 667 | if(strlen($m) === 1) 668 | $capturingGroupCount++; 669 | 670 | $groupStyleDepth = ($groupStyleDepth !== 5) ? $groupStyleDepth + 1 : 1; 671 | $openGroups[] = $m; // opening 672 | $lastIsQuant = false; 673 | $output[] = array("g{$groupStyleDepth}" => $m); 674 | 675 | }elseif($m[0] === ')'){ 676 | if(!count($openGroups)) 677 | throw new \Exception('No matching opening parenthesis'); 678 | 679 | $output[] = array('g' . $groupStyleDepth => ')'); 680 | $prevGroup = $openGroups[count($openGroups) - 1]; 681 | $prevGroup = isset($prevGroup[2]) ? $prevGroup[2] : ''; 682 | $lastIsQuant = !preg_match('/^[=!]/', $prevGroup); 683 | $lastStyle = "g{$groupStyleDepth}"; 684 | $lastType = 0; 685 | $groupStyleDepth = ($groupStyleDepth !== 1) ? $groupStyleDepth - 1 : 5; 686 | 687 | array_pop($openGroups); 688 | continue; 689 | 690 | }elseif($m[0] === '\\'){ 691 | if(isset($m[1]) && preg_match('/^[1-9]/', $m[1])){ 692 | $nonBackrefDigits = ''; 693 | $num = substr(+$m, 1); 694 | 695 | while($num > $capturingGroupCount){ 696 | preg_match('/[0-9]$/', $num, $digits); 697 | $nonBackrefDigits = $digits[0] . $nonBackrefDigits; 698 | $num = floor($num / 10); 699 | } 700 | 701 | if($num > 0){ 702 | $output[] = array('meta' => "\\{$num}", 'text' => $nonBackrefDigits); 703 | 704 | }else{ 705 | preg_match('/^\\\\([0-3][0-7]{0,2}|[4-7][0-7]?|[89])([0-9]*)/', $m, $pts); 706 | $output[] = array('meta' => '\\' . $pts[1], 'text' => $pts[2]); 707 | } 708 | 709 | $lastIsQuant = true; 710 | 711 | }elseif(isset($m[1]) && preg_match('/^[0bBcdDfnrsStuvwWx]/', $m[1])){ 712 | 713 | if(preg_match('/^\\\\[cux]$/', $m)) 714 | throw new \Exception('Incomplete regex token'); 715 | 716 | $output[] = array('meta' => $m); 717 | $lastIsQuant = (strpos('bB', $m[1]) === false); 718 | 719 | }elseif($m === '\\'){ 720 | throw new \Exception('Incomplete regex token'); 721 | 722 | }else{ 723 | $output[] = array('text' => $m); 724 | $lastIsQuant = true; 725 | } 726 | 727 | }elseif(preg_match('/^(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??$/', $m)){ 728 | if(!$lastIsQuant) 729 | throw new \Exception('Quantifiers must be preceded by a token that can be repeated'); 730 | 731 | preg_match('/^\{([0-9]+)(?:,([0-9]*))?/', $m, $interval); 732 | 733 | if($interval && (+$interval[1] > 65535 || (isset($interval[2]) && (+$interval[2] > 65535)))) 734 | throw new \Exception('Interval quantifier cannot use value over 65,535'); 735 | 736 | if($interval && isset($interval[2]) && (+$interval[1] > +$interval[2])) 737 | throw new \Exception('Interval quantifier range is reversed'); 738 | 739 | $output[] = array($lastStyle ? $lastStyle : 'meta' => $m); 740 | $lastIsQuant = false; 741 | 742 | }elseif($m === '|'){ 743 | if($lastType === 1 || ($lastType === 2 && !count($openGroups))) 744 | throw new \Exception('Empty alternative effectively truncates the regex here'); 745 | 746 | $output[] = count($openGroups) ? array("g{$groupStyleDepth}" => '|') : array('meta' => '|'); 747 | $lastIsQuant = false; 748 | $lastType = 2; 749 | $lastStyle = ''; 750 | continue; 751 | 752 | }elseif($m === '^' || $m === '$'){ 753 | $output[] = array('meta' => $m); 754 | $lastIsQuant = false; 755 | 756 | }elseif($m === '.'){ 757 | $output[] = array('meta' => '.'); 758 | $lastIsQuant = true; 759 | 760 | }else{ 761 | $output[] = array('text' => $m); 762 | $lastIsQuant = true; 763 | } 764 | 765 | $lastType = 0; 766 | $lastStyle = ''; 767 | } 768 | 769 | if($openGroups) 770 | throw new \Exception('Unclosed grouping'); 771 | 772 | return $output; 773 | } 774 | 775 | 776 | 777 | /** 778 | * Set or get configuration options 779 | * 780 | * @param string $key 781 | * @param mixed|null $value 782 | * @return mixed 783 | */ 784 | public static function config($key, $value = null){ 785 | 786 | if(!array_key_exists($key, static::$config)) 787 | throw new \Exception(sprintf('Unrecognized option: "%s". Valid options are: %s', $key, implode(', ', array_keys(static::$config)))); 788 | 789 | if($value === null) 790 | return static::$config[$key]; 791 | 792 | if(is_array(static::$config[$key])) 793 | return static::$config[$key] = (array)$value; 794 | 795 | return static::$config[$key] = $value; 796 | } 797 | 798 | 799 | 800 | /** 801 | * Total CPU time used by the class 802 | * 803 | * @param int precision 804 | * @return double 805 | */ 806 | public static function getTime($precision = 4){ 807 | return round(static::$time, $precision); 808 | } 809 | 810 | 811 | 812 | /** 813 | * Get relevant backtrace info for last ref call 814 | * 815 | * @return array|false 816 | */ 817 | public static function getBacktrace(){ 818 | 819 | // pull only basic info with php 5.3.6+ to save some memory 820 | $trace = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace(); 821 | 822 | while($callee = array_pop($trace)){ 823 | 824 | // extract only the information we neeed 825 | $callee = array_intersect_key($callee, array_fill_keys(array('file', 'function', 'line'), false)); 826 | extract($callee, EXTR_OVERWRITE); 827 | 828 | // skip, if the called function doesn't match the shortcut function name 829 | if(!$function || !in_array(mb_strtolower((string)$function), static::$config['shortcutFunc'])) 830 | continue; 831 | 832 | return compact('file', 'function', 'line'); 833 | } 834 | 835 | return false; 836 | } 837 | 838 | 839 | 840 | /** 841 | * Determines the input expression(s) passed to the shortcut function 842 | * 843 | * @param array &$options Optional, options to gather (from operators) 844 | * @return array Array of string expressions 845 | */ 846 | public static function getInputExpressions(array &$options = null){ 847 | 848 | // used to determine the position of the current call, 849 | // if more queries calls were made on the same line 850 | static $lineInst = array(); 851 | 852 | $trace = static::getBacktrace(); 853 | 854 | if(!$trace) 855 | return array(); 856 | 857 | extract($trace); 858 | 859 | $code = file($file); 860 | $code = $code[$line - 1]; // multiline expressions not supported! 861 | $instIndx = 0; 862 | $tokens = token_get_all(" $token){ 866 | 867 | // match token with our shortcut function name 868 | if(is_string($token) || ($token[0] !== T_STRING) || (strcasecmp($token[1], $function) !== 0)) 869 | continue; 870 | 871 | // is this some method that happens to have the same name as the shortcut function? 872 | if(isset($tokens[$i - 1]) && is_array($tokens[$i - 1]) && in_array($tokens[$i - 1][0], array(T_DOUBLE_COLON, T_OBJECT_OPERATOR), true)) 873 | continue; 874 | 875 | // find argument definition start, just after '(' 876 | if(isset($tokens[$i + 1]) && ($tokens[$i + 1][0] === '(')){ 877 | $instIndx++; 878 | 879 | if(!isset($lineInst[$line])) 880 | $lineInst[$line] = 0; 881 | 882 | if($instIndx <= $lineInst[$line]) 883 | continue; 884 | 885 | $lineInst[$line]++; 886 | 887 | // gather options 888 | if($options !== null){ 889 | $j = $i - 1; 890 | while(isset($tokens[$j]) && is_string($tokens[$j]) && in_array($tokens[$j], array('@', '+', '-', '!', '~'))) 891 | $options[] = $tokens[$j--]; 892 | } 893 | 894 | $lvl = $index = $curlies = 0; 895 | $expressions = array(); 896 | 897 | // get the expressions 898 | foreach(array_slice($tokens, $i + 2) as $token){ 899 | 900 | if(is_array($token)){ 901 | if($token[0] !== T_COMMENT) 902 | $expressions[$index][] = ($token[0] !== T_WHITESPACE) ? $token[1] : ' '; 903 | 904 | continue; 905 | } 906 | 907 | if($token === '{') 908 | $curlies++; 909 | 910 | if($token === '}') 911 | $curlies--; 912 | 913 | if($token === '(') 914 | $lvl++; 915 | 916 | if($token === ')') 917 | $lvl--; 918 | 919 | // assume next argument if a comma was encountered, 920 | // and we're not insde a curly bracket or inner parentheses 921 | if(($curlies < 1) && ($lvl === 0) && ($token === ',')){ 922 | $index++; 923 | continue; 924 | } 925 | 926 | // negative parentheses count means we reached the end of argument definitions 927 | if($lvl < 0){ 928 | foreach($expressions as &$expression) 929 | $expression = trim(implode('', $expression)); 930 | 931 | return $expressions; 932 | } 933 | 934 | $expressions[$index][] = $token; 935 | } 936 | 937 | break; 938 | } 939 | } 940 | 941 | return array(); 942 | } 943 | 944 | 945 | 946 | /** 947 | * Get all parent classes of a class 948 | * 949 | * @param Reflector $class Reflection object 950 | * @return array Array of ReflectionClass objects (starts with the ancestor, ends with the given class) 951 | */ 952 | protected static function getParentClasses(\Reflector $class){ 953 | 954 | $parents = array($class); 955 | while(($class = $class->getParentClass()) !== false) 956 | $parents[] = $class; 957 | 958 | return array_reverse($parents); 959 | } 960 | 961 | 962 | 963 | /** 964 | * Generate class / function info 965 | * 966 | * @param Reflector $reflector Class name or reflection object 967 | * @param string $single Skip parent classes 968 | * @param Reflector|null $context Object context (for methods) 969 | * @return string 970 | */ 971 | protected function fromReflector(\Reflector $reflector, $single = '', \Reflector $context = null){ 972 | 973 | // @todo: test this 974 | $hash = var_export(func_get_args(), true); 975 | //$hash = $reflector->getName() . ';' . $single . ';' . ($context ? $context->getName() : ''); 976 | 977 | if($this->fmt->didCache($hash)){ 978 | static::$debug['cacheHits']++; 979 | return; 980 | } 981 | 982 | $items = array($reflector); 983 | 984 | if(($single === '') && ($reflector instanceof \ReflectionClass)) 985 | $items = static::getParentClasses($reflector); 986 | 987 | $first = true; 988 | foreach($items as $item){ 989 | 990 | if(!$first) 991 | $this->fmt->sep(' :: '); 992 | 993 | $first = false; 994 | $name = ($single !== '') ? $single : $item->getName(); 995 | $comments = $item->isInternal() ? array() : static::parseComment($item->getDocComment()); 996 | $meta = array('sub' => array()); 997 | $bubbles = array(); 998 | 999 | if($item->isInternal()){ 1000 | $extension = $item->getExtension(); 1001 | $meta['title'] = ($extension instanceof \ReflectionExtension) ? sprintf('Internal - part of %s (%s)', $extension->getName(), $extension->getVersion()) : 'Internal'; 1002 | 1003 | }else{ 1004 | $comments = static::parseComment($item->getDocComment()); 1005 | 1006 | if($comments) 1007 | $meta += $comments; 1008 | 1009 | $meta['sub'][] = array('Defined in', basename($item->getFileName()) . ':' . $item->getStartLine()); 1010 | } 1011 | 1012 | if(($item instanceof \ReflectionFunction) || ($item instanceof \ReflectionMethod)){ 1013 | if(($context !== null) && ($context->getShortName() !== $item->getDeclaringClass()->getShortName())) 1014 | $meta['sub'][] = array('Inherited from', $item->getDeclaringClass()->getShortName()); 1015 | 1016 | // @note: PHP 7 seems to crash when calling getPrototype on Closure::__invoke() 1017 | if(($item instanceof \ReflectionMethod) && !$item->isInternal()){ 1018 | try{ 1019 | $proto = $item->getPrototype(); 1020 | $meta['sub'][] = array('Prototype defined by', $proto->class); 1021 | }catch(\Exception $e){} 1022 | } 1023 | 1024 | $this->fmt->text('name', $name, $meta, $this->linkify($item)); 1025 | continue; 1026 | } 1027 | 1028 | // @todo: maybe - list interface methods 1029 | if(!($item->isInterface() || (static::$env['is54'] && $item->isTrait()))){ 1030 | 1031 | if($item->isAbstract()) 1032 | $bubbles[] = array('A', 'Abstract'); 1033 | 1034 | if(static::$env['is7'] && $item->isAnonymous()) 1035 | $bubbles[] = array('?', 'Anonymous'); 1036 | 1037 | if($item->isFinal()) 1038 | $bubbles[] = array('F', 'Final'); 1039 | 1040 | // php 5.4+ only 1041 | if(static::$env['is54'] && $item->isCloneable()) 1042 | $bubbles[] = array('C', 'Cloneable'); 1043 | 1044 | if($item->isIterateable()) 1045 | $bubbles[] = array('X', 'Iterateable'); 1046 | 1047 | } 1048 | 1049 | if($item->isInterface() && $single !== '') 1050 | $bubbles[] = array('I', 'Interface'); 1051 | 1052 | if($bubbles) 1053 | $this->fmt->bubbles($bubbles); 1054 | 1055 | if($item->isInterface() && $single === '') 1056 | $name .= sprintf(' (%d)', count($item->getMethods())); 1057 | 1058 | $this->fmt->text('name', $name, $meta, $this->linkify($item)); 1059 | } 1060 | 1061 | $this->fmt->cacheLock($hash); 1062 | } 1063 | 1064 | 1065 | 1066 | /** 1067 | * Generates an URL that points to the documentation page relevant for the requested context 1068 | * 1069 | * For internal functions and classes, the URI will point to the local PHP manual 1070 | * if installed and configured, otherwise to php.net/manual (the english one) 1071 | * 1072 | * @param Reflector $reflector Reflector object (used to determine the URL scheme for internal stuff) 1073 | * @param string|null $constant Constant name, if this is a request to linkify a constant 1074 | * @return string|null URL 1075 | */ 1076 | protected function linkify(\Reflector $reflector, $constant = null){ 1077 | 1078 | static $docRefRoot = null, $docRefExt = null; 1079 | 1080 | // most people don't have this set 1081 | if(!$docRefRoot) 1082 | $docRefRoot = ($docRefRoot = rtrim(ini_get('docref_root'), '/')) ? $docRefRoot : 'http://php.net/manual/en'; 1083 | 1084 | if(!$docRefExt) 1085 | $docRefExt = ($docRefExt = ini_get('docref_ext')) ? $docRefExt : '.php'; 1086 | 1087 | $phpNetSchemes = array( 1088 | 'class' => $docRefRoot . '/class.%s' . $docRefExt, 1089 | 'function' => $docRefRoot . '/function.%s' . $docRefExt, 1090 | 'method' => $docRefRoot . '/%2$s.%1$s' . $docRefExt, 1091 | 'property' => $docRefRoot . '/class.%2$s' . $docRefExt . '#%2$s.props.%1$s', 1092 | 'constant' => $docRefRoot . '/class.%2$s' . $docRefExt . '#%2$s.constants.%1$s', 1093 | ); 1094 | 1095 | $url = null; 1096 | $args = array(); 1097 | 1098 | // determine scheme 1099 | if($constant !== null){ 1100 | $type = 'constant'; 1101 | $args[] = $constant; 1102 | 1103 | }else{ 1104 | $type = explode('\\', get_class($reflector)); 1105 | $type = strtolower(ltrim(end($type), 'Reflection')); 1106 | 1107 | if($type === 'object') 1108 | $type = 'class'; 1109 | } 1110 | 1111 | // properties don't have the internal flag; 1112 | // also note that many internal classes use some kind of magic as properties (eg. DateTime); 1113 | // these will only get linkifed if the declared class is internal one, and not an extension :( 1114 | $parent = ($type !== 'property') ? $reflector : $reflector->getDeclaringClass(); 1115 | 1116 | // internal function/method/class/property/constant 1117 | if($parent->isInternal()){ 1118 | $args[] = $reflector->name; 1119 | 1120 | if(in_array($type, array('method', 'property'), true)) 1121 | $args[] = $reflector->getDeclaringClass()->getName(); 1122 | 1123 | $args = array_map(function($text){ 1124 | return str_replace('_', '-', ltrim(strtolower($text), '\\_')); 1125 | }, $args); 1126 | 1127 | // check for some special cases that have no links 1128 | $valid = (($type === 'method') || (strcasecmp($parent->name, 'stdClass') !== 0)) 1129 | && (($type !== 'method') || (($reflector->name === '__construct') || strpos($reflector->name, '__') !== 0)); 1130 | 1131 | if($valid) 1132 | $url = vsprintf($phpNetSchemes[$type], $args); 1133 | 1134 | // custom 1135 | }else{ 1136 | switch(true){ 1137 | 1138 | // WordPress function; 1139 | // like pretty much everything else in WordPress, API links are inconsistent as well; 1140 | // so we're using queryposts.com as doc source for API 1141 | case ($type === 'function') && class_exists('WP', false) && defined('ABSPATH') && defined('WPINC'): 1142 | if(strpos($reflector->getFileName(), realpath(ABSPATH . WPINC)) === 0){ 1143 | $url = sprintf('http://queryposts.com/function/%s', urlencode(strtolower($reflector->getName()))); 1144 | break; 1145 | } 1146 | 1147 | // @todo: handle more apps 1148 | } 1149 | 1150 | } 1151 | 1152 | return $url; 1153 | } 1154 | 1155 | 1156 | public static function getTimeoutPoint(){ 1157 | return static::$timeout; 1158 | } 1159 | 1160 | 1161 | public static function getDebugInfo(){ 1162 | return static::$debug; 1163 | } 1164 | 1165 | 1166 | 1167 | protected function hasInstanceTimedOut(){ 1168 | 1169 | if(static::$timeout > 0) 1170 | return true; 1171 | 1172 | $timeout = static::$config['timeout']; 1173 | 1174 | if(($timeout > 0) && ((microtime(true) - $this->startTime) > $timeout)) 1175 | return (static::$timeout = (microtime(true) - $this->startTime)); 1176 | 1177 | return false; 1178 | } 1179 | 1180 | 1181 | 1182 | /** 1183 | * Evaluates the given variable 1184 | * 1185 | * @param mixed &$subject Variable to query 1186 | * @param bool $specialStr Should this be interpreted as a special string? 1187 | * @return mixed Result (both HTML and text modes generate strings) 1188 | */ 1189 | protected function evaluate(&$subject, $specialStr = false){ 1190 | 1191 | switch($type = gettype($subject)){ 1192 | 1193 | // https://github.com/digitalnature/php-ref/issues/13 1194 | case 'unknown type': 1195 | return $this->fmt->text('unknown'); 1196 | 1197 | // null value 1198 | case 'NULL': 1199 | return $this->fmt->text('null'); 1200 | 1201 | // integer/double/float 1202 | case 'integer': 1203 | case 'double': 1204 | return $this->fmt->text($type, $subject, $type); 1205 | 1206 | // boolean 1207 | case 'boolean': 1208 | $text = $subject ? 'true' : 'false'; 1209 | return $this->fmt->text($text, $text, $type); 1210 | 1211 | // arrays 1212 | case 'array': 1213 | 1214 | // empty array? 1215 | if(empty($subject)){ 1216 | $this->fmt->text('array'); 1217 | return $this->fmt->emptyGroup(); 1218 | } 1219 | 1220 | if(isset($subject[static::MARKER_KEY])){ 1221 | unset($subject[static::MARKER_KEY]); 1222 | $this->fmt->text('array'); 1223 | $this->fmt->emptyGroup('recursion'); 1224 | return; 1225 | } 1226 | 1227 | // first recursion level detection; 1228 | // this is optional (used to print consistent recursion info) 1229 | foreach($subject as $key => &$value){ 1230 | 1231 | if(!is_array($value)) 1232 | continue; 1233 | 1234 | // save current value in a temporary variable 1235 | $buffer = $value; 1236 | 1237 | // assign new value 1238 | $value = ($value !== 1) ? 1 : 2; 1239 | 1240 | // if they're still equal, then we have a reference 1241 | if($value === $subject){ 1242 | $value = $buffer; 1243 | $value[static::MARKER_KEY] = true; 1244 | $this->evaluate($value); 1245 | return; 1246 | } 1247 | 1248 | // restoring original value 1249 | $value = $buffer; 1250 | } 1251 | 1252 | $this->fmt->text('array'); 1253 | $count = count($subject); 1254 | if(!$this->fmt->startGroup($count)) 1255 | return; 1256 | 1257 | $max = max(array_map('static::strLen', array_keys($subject))); 1258 | $subject[static::MARKER_KEY] = true; 1259 | 1260 | foreach($subject as $key => &$value){ 1261 | 1262 | // ignore our temporary marker 1263 | if($key === static::MARKER_KEY) 1264 | continue; 1265 | 1266 | if($this->hasInstanceTimedOut()) 1267 | break; 1268 | 1269 | $keyInfo = gettype($key); 1270 | 1271 | if($keyInfo === 'string'){ 1272 | $encoding = static::$env['mbStr'] ? mb_detect_encoding($key) : ''; 1273 | $keyLen = static::strLen($key); 1274 | $keyLenInfo = $encoding && ($encoding !== 'ASCII') ? $keyLen . '; ' . $encoding : $keyLen; 1275 | $keyInfo = "{$keyInfo}({$keyLenInfo})"; 1276 | }else{ 1277 | $keyLen = strlen($key); 1278 | } 1279 | 1280 | $this->fmt->startRow(); 1281 | $this->fmt->text('key', $key, "Key: {$keyInfo}"); 1282 | $this->fmt->colDiv($max - $keyLen); 1283 | $this->fmt->sep('=>'); 1284 | $this->fmt->colDiv(); 1285 | $this->evaluate($value, $specialStr); 1286 | $this->fmt->endRow(); 1287 | } 1288 | 1289 | unset($subject[static::MARKER_KEY]); 1290 | 1291 | $this->fmt->endGroup(); 1292 | return; 1293 | 1294 | // resource 1295 | case 'resource': 1296 | case 'resource (closed)': 1297 | $meta = array(); 1298 | $resType = get_resource_type($subject); 1299 | 1300 | $this->fmt->text('resource', strval($subject)); 1301 | 1302 | if(!static::$config['showResourceInfo']) 1303 | return $this->fmt->emptyGroup($resType); 1304 | 1305 | // @see: http://php.net/manual/en/resource.php 1306 | // need to add more... 1307 | switch($resType){ 1308 | 1309 | // curl extension resource 1310 | case 'curl': 1311 | $meta = curl_getinfo($subject); 1312 | break; 1313 | 1314 | case 'FTP Buffer': 1315 | $meta = array( 1316 | 'time_out' => ftp_get_option($subject, FTP_TIMEOUT_SEC), 1317 | 'auto_seek' => ftp_get_option($subject, FTP_AUTOSEEK), 1318 | ); 1319 | 1320 | break; 1321 | 1322 | // gd image extension resource 1323 | case 'gd': 1324 | $meta = array( 1325 | 'size' => sprintf('%d x %d', imagesx($subject), imagesy($subject)), 1326 | 'true_color' => imageistruecolor($subject), 1327 | ); 1328 | 1329 | break; 1330 | 1331 | case 'ldap link': 1332 | $constants = get_defined_constants(); 1333 | 1334 | array_walk($constants, function($value, $key) use(&$constants){ 1335 | if(strpos($key, 'LDAP_OPT_') !== 0) 1336 | unset($constants[$key]); 1337 | }); 1338 | 1339 | // this seems to fail on my setup :( 1340 | unset($constants['LDAP_OPT_NETWORK_TIMEOUT']); 1341 | 1342 | foreach(array_slice($constants, 3) as $key => $value) 1343 | if(ldap_get_option($subject, (int)$value, $ret)) 1344 | $meta[strtolower(substr($key, 9))] = $ret; 1345 | 1346 | break; 1347 | 1348 | // mysql connection (mysql extension is deprecated from php 5.4/5.5) 1349 | case 'mysql link': 1350 | case 'mysql link persistent': 1351 | $dbs = array(); 1352 | $query = @mysql_list_dbs($subject); 1353 | while($row = @mysql_fetch_array($query)) 1354 | $dbs[] = $row['Database']; 1355 | 1356 | $meta = array( 1357 | 'host' => ltrim(@mysql_get_host_info ($subject), 'MySQL host info: '), 1358 | 'server_version' => @mysql_get_server_info($subject), 1359 | 'protocol_version' => @mysql_get_proto_info($subject), 1360 | 'databases' => $dbs, 1361 | ); 1362 | 1363 | break; 1364 | 1365 | // mysql result 1366 | case 'mysql result': 1367 | while($row = @mysql_fetch_object($subject)){ 1368 | $meta[] = (array)$row; 1369 | 1370 | if($this->hasInstanceTimedOut()) 1371 | break; 1372 | } 1373 | 1374 | break; 1375 | 1376 | // stream resource (fopen, fsockopen, popen, opendir etc) 1377 | case 'stream': 1378 | $meta = stream_get_meta_data($subject); 1379 | break; 1380 | 1381 | } 1382 | 1383 | if(!$meta) 1384 | return $this->fmt->emptyGroup($resType); 1385 | 1386 | 1387 | if(!$this->fmt->startGroup($resType)) 1388 | return; 1389 | 1390 | $max = max(array_map('static::strLen', array_keys($meta))); 1391 | foreach($meta as $key => $value){ 1392 | $this->fmt->startRow(); 1393 | $this->fmt->text('resourceProp', ucwords(str_replace('_', ' ', $key))); 1394 | $this->fmt->colDiv($max - static::strLen($key)); 1395 | $this->fmt->sep(':'); 1396 | $this->fmt->colDiv(); 1397 | $this->evaluate($value); 1398 | $this->fmt->endRow(); 1399 | } 1400 | $this->fmt->endGroup(); 1401 | return; 1402 | 1403 | // string 1404 | case 'string': 1405 | $length = static::strLen($subject); 1406 | $encoding = static::$env['mbStr'] ? mb_detect_encoding($subject) : false; 1407 | $info = $encoding && ($encoding !== 'ASCII') ? $length . '; ' . $encoding : $length; 1408 | 1409 | if($specialStr){ 1410 | $this->fmt->sep('"'); 1411 | $this->fmt->text(array('string', 'special'), $subject, "string({$info})"); 1412 | $this->fmt->sep('"'); 1413 | return; 1414 | } 1415 | 1416 | $this->fmt->text('string', $subject, "string({$info})"); 1417 | 1418 | // advanced checks only if there are 3 characteres or more 1419 | if(static::$config['showStringMatches'] && ($length > 2) && (trim($subject) !== '')){ 1420 | 1421 | $isNumeric = is_numeric($subject); 1422 | 1423 | // very simple check to determine if the string could match a file path 1424 | // @note: this part of the code is very expensive 1425 | $isFile = ($length < 2048) 1426 | && (max(array_map('strlen', explode('/', str_replace('\\', '/', $subject)))) < 128) 1427 | && !preg_match('/[^\w\.\-\/\\\\:]|\..*\.|\.$|:(?!(?<=^[a-zA-Z]:)[\/\\\\])/', $subject); 1428 | 1429 | if($isFile){ 1430 | try{ 1431 | $file = new \SplFileInfo($subject); 1432 | $flags = array(); 1433 | $perms = $file->getPerms(); 1434 | 1435 | if(($perms & 0xC000) === 0xC000) // socket 1436 | $flags[] = 's'; 1437 | elseif(($perms & 0xA000) === 0xA000) // symlink 1438 | $flags[] = 'l'; 1439 | elseif(($perms & 0x8000) === 0x8000) // regular 1440 | $flags[] = '-'; 1441 | elseif(($perms & 0x6000) === 0x6000) // block special 1442 | $flags[] = 'b'; 1443 | elseif(($perms & 0x4000) === 0x4000) // directory 1444 | $flags[] = 'd'; 1445 | elseif(($perms & 0x2000) === 0x2000) // character special 1446 | $flags[] = 'c'; 1447 | elseif(($perms & 0x1000) === 0x1000) // FIFO pipe 1448 | $flags[] = 'p'; 1449 | else // unknown 1450 | $flags[] = 'u'; 1451 | 1452 | // owner 1453 | $flags[] = (($perms & 0x0100) ? 'r' : '-'); 1454 | $flags[] = (($perms & 0x0080) ? 'w' : '-'); 1455 | $flags[] = (($perms & 0x0040) ? (($perms & 0x0800) ? 's' : 'x' ) : (($perms & 0x0800) ? 'S' : '-')); 1456 | 1457 | // group 1458 | $flags[] = (($perms & 0x0020) ? 'r' : '-'); 1459 | $flags[] = (($perms & 0x0010) ? 'w' : '-'); 1460 | $flags[] = (($perms & 0x0008) ? (($perms & 0x0400) ? 's' : 'x' ) : (($perms & 0x0400) ? 'S' : '-')); 1461 | 1462 | // world 1463 | $flags[] = (($perms & 0x0004) ? 'r' : '-'); 1464 | $flags[] = (($perms & 0x0002) ? 'w' : '-'); 1465 | $flags[] = (($perms & 0x0001) ? (($perms & 0x0200) ? 't' : 'x' ) : (($perms & 0x0200) ? 'T' : '-')); 1466 | 1467 | $size = is_dir($subject) ? '' : sprintf(' %.2fK', $file->getSize() / 1024); 1468 | 1469 | $this->fmt->startContain('file', true); 1470 | $this->fmt->text('file', implode('', $flags) . $size); 1471 | $this->fmt->endContain(); 1472 | 1473 | }catch(\Exception $e){ 1474 | $isFile = false; 1475 | } 1476 | } 1477 | 1478 | // class/interface/function 1479 | if(!preg_match('/[^\w+\\\\]/', $subject) && ($length < 96)){ 1480 | $isClass = class_exists($subject, false); 1481 | if($isClass){ 1482 | $this->fmt->startContain('class', true); 1483 | $this->fromReflector(new \ReflectionClass($subject)); 1484 | $this->fmt->endContain(); 1485 | } 1486 | 1487 | if(!$isClass && interface_exists($subject, false)){ 1488 | $this->fmt->startContain('interface', true); 1489 | $this->fromReflector(new \ReflectionClass($subject)); 1490 | $this->fmt->endContain('interface'); 1491 | } 1492 | 1493 | if(function_exists($subject)){ 1494 | $this->fmt->startContain('function', true); 1495 | $this->fromReflector(new \ReflectionFunction($subject)); 1496 | $this->fmt->endContain('function'); 1497 | } 1498 | } 1499 | 1500 | 1501 | // skip serialization/json/date checks if the string appears to be numeric, 1502 | // or if it's shorter than 5 characters 1503 | if(!$isNumeric && ($length > 4)){ 1504 | 1505 | // url 1506 | if(static::$config['showUrls'] && static::$env['curlActive'] && filter_var($subject, FILTER_VALIDATE_URL)){ 1507 | $ch = curl_init($subject); 1508 | curl_setopt($ch, CURLOPT_NOBODY, true); 1509 | curl_exec($ch); 1510 | $nfo = curl_getinfo($ch); 1511 | curl_close($ch); 1512 | 1513 | if($nfo['http_code']){ 1514 | $this->fmt->startContain('url', true); 1515 | $contentType = explode(';', $nfo['content_type']); 1516 | $this->fmt->text('url', sprintf('%s:%d %s %.2fms (%d)', !empty($nfo['primary_ip']) ? $nfo['primary_ip'] : null, !empty($nfo['primary_port']) ? $nfo['primary_port'] : null, $contentType[0], $nfo['total_time'], $nfo['http_code'])); 1517 | $this->fmt->endContain(); 1518 | } 1519 | 1520 | } 1521 | 1522 | // date 1523 | if(($length < 128) && static::$env['supportsDate'] && !preg_match('/[^A-Za-z0-9.:+\s\-\/]/', $subject)){ 1524 | try{ 1525 | $date = new \DateTime($subject); 1526 | $errors = \DateTime::getLastErrors(); 1527 | 1528 | if(($errors['warning_count'] < 1) && ($errors['error_count'] < 1)){ 1529 | $now = new \Datetime('now'); 1530 | $nowUtc = new \Datetime('now', new \DateTimeZone('UTC')); 1531 | $diff = $now->diff($date); 1532 | 1533 | $map = array( 1534 | 'y' => 'yr', 1535 | 'm' => 'mo', 1536 | 'd' => 'da', 1537 | 'h' => 'hr', 1538 | 'i' => 'min', 1539 | 's' => 'sec', 1540 | ); 1541 | 1542 | $timeAgo = 'now'; 1543 | foreach($map as $k => $label){ 1544 | if($diff->{$k} > 0){ 1545 | $timeAgo = $diff->format("%R%{$k}{$label}"); 1546 | break; 1547 | } 1548 | } 1549 | 1550 | $tz = $date->getTimezone(); 1551 | $offs = round($tz->getOffset($nowUtc) / 3600); 1552 | 1553 | if($offs > 0) 1554 | $offs = "+{$offs}"; 1555 | 1556 | $timeAgo .= ((int)$offs !== 0) ? ' ' . sprintf('%s (UTC%s)', $tz->getName(), $offs) : ' UTC'; 1557 | $this->fmt->startContain('date', true); 1558 | $this->fmt->text('date', $timeAgo); 1559 | $this->fmt->endContain(); 1560 | 1561 | } 1562 | }catch(\Exception $e){ 1563 | // not a date 1564 | } 1565 | 1566 | } 1567 | 1568 | // attempt to detect if this is a serialized string 1569 | static $unserializing = 0; 1570 | $isSerialized = ($unserializing < 3) 1571 | && (($subject[$length - 1] === ';') || ($subject[$length - 1] === '}')) 1572 | && in_array($subject[0], array('s', 'a', 'O'), true) 1573 | && ((($subject[0] === 's') && ($subject[$length - 2] !== '"')) || preg_match("/^{$subject[0]}:[0-9]+:/s", $subject)) 1574 | && (($unserialized = @unserialize($subject)) !== false); 1575 | 1576 | if($isSerialized){ 1577 | $unserializing++; 1578 | $this->fmt->startContain('serialized', true); 1579 | $this->evaluate($unserialized); 1580 | $this->fmt->endContain(); 1581 | $unserializing--; 1582 | } 1583 | 1584 | // try to find out if it's a json-encoded string; 1585 | // only do this for json-encoded arrays or objects, because other types have too generic formats 1586 | static $decodingJson = 0; 1587 | $isJson = !$isSerialized && ($decodingJson < 3) && in_array($subject[0], array('{', '['), true); 1588 | 1589 | if($isJson){ 1590 | $decodingJson++; 1591 | $data = json_decode($subject); 1592 | 1593 | // ensure created objects live enough for PHP to provide a unique hash 1594 | if(is_object($data)) 1595 | $this->intObjects->attach($data); 1596 | 1597 | if($isJson = (json_last_error() === JSON_ERROR_NONE)){ 1598 | $this->fmt->startContain('json', true); 1599 | $this->evaluate($data); 1600 | $this->fmt->endContain(); 1601 | } 1602 | 1603 | $decodingJson--; 1604 | } 1605 | 1606 | // attempt to match a regex 1607 | if(!$isSerialized && !$isJson && $length < 768){ 1608 | try{ 1609 | $components = $this->splitRegex($subject); 1610 | if($components){ 1611 | $regex = ''; 1612 | 1613 | $this->fmt->startContain('regex', true); 1614 | foreach($components as $component) 1615 | $this->fmt->text('regex-' . key($component), reset($component)); 1616 | $this->fmt->endContain(); 1617 | } 1618 | 1619 | }catch(\Exception $e){ 1620 | // not a regex 1621 | } 1622 | 1623 | } 1624 | } 1625 | } 1626 | 1627 | return; 1628 | } 1629 | 1630 | // if we reached this point, $subject must be an object 1631 | 1632 | // track objects to detect recursion 1633 | static $hashes = array(); 1634 | 1635 | // hash ID of this object 1636 | $hash = spl_object_hash($subject); 1637 | $recursion = isset($hashes[$hash]); 1638 | 1639 | // sometimes incomplete objects may be created from string unserialization, 1640 | // if the class to which the object belongs wasn't included until the unserialization stage... 1641 | if($subject instanceof \__PHP_Incomplete_Class){ 1642 | $this->fmt->text('object'); 1643 | $this->fmt->emptyGroup('incomplete'); 1644 | return; 1645 | } 1646 | 1647 | // check cache at this point 1648 | if(!$recursion && $this->fmt->didCache($hash)){ 1649 | static::$debug['cacheHits']++; 1650 | return; 1651 | } 1652 | 1653 | $reflector = new \ReflectionObject($subject); 1654 | $this->fmt->startContain('class'); 1655 | $this->fromReflector($reflector); 1656 | $this->fmt->text('object', ' object'); 1657 | $this->fmt->endContain(); 1658 | 1659 | // already been here? 1660 | if($recursion) 1661 | return $this->fmt->emptyGroup('recursion'); 1662 | 1663 | $hashes[$hash] = 1; 1664 | 1665 | $flags = \ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED; 1666 | 1667 | if(static::$config['showPrivateMembers']) 1668 | $flags |= \ReflectionProperty::IS_PRIVATE; 1669 | 1670 | $props = $magicProps = $methods = array(); 1671 | 1672 | if($reflector->hasMethod('__debugInfo')){ 1673 | $magicProps = $subject->__debugInfo(); 1674 | }else{ 1675 | $props = $reflector->getProperties($flags); 1676 | } 1677 | 1678 | if(static::$config['showMethods']){ 1679 | $flags = \ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED; 1680 | 1681 | if(static::$config['showPrivateMembers']) 1682 | $flags |= \ReflectionMethod::IS_PRIVATE; 1683 | 1684 | $methods = $reflector->getMethods($flags); 1685 | } 1686 | 1687 | $constants = $reflector->getConstants(); 1688 | $interfaces = $reflector->getInterfaces(); 1689 | $traits = static::$env['is54'] ? $reflector->getTraits() : array(); 1690 | $parents = static::getParentClasses($reflector); 1691 | 1692 | // work-around for https://bugs.php.net/bug.php?id=49154 1693 | // @see http://stackoverflow.com/questions/15672287/strange-behavior-of-reflectiongetproperties-with-numeric-keys 1694 | if(!static::$env['is54']){ 1695 | $props = array_values(array_filter($props, function($prop) use($subject){ 1696 | return !$prop->isPublic() || property_exists($subject, $prop->name); 1697 | })); 1698 | } 1699 | 1700 | // no data to display? 1701 | if(!$props && !$methods && !$constants && !$interfaces && !$traits){ 1702 | unset($hashes[$hash]); 1703 | return $this->fmt->emptyGroup(); 1704 | } 1705 | 1706 | if(!$this->fmt->startGroup()) 1707 | return; 1708 | 1709 | // show contents for iterators 1710 | if(static::$config['showIteratorContents'] && $reflector->isIterateable()){ 1711 | 1712 | $itContents = iterator_to_array($subject); 1713 | $this->fmt->sectionTitle(sprintf('Contents (%d)', count($itContents))); 1714 | 1715 | foreach($itContents as $key => $value){ 1716 | $keyInfo = gettype($key); 1717 | if($keyInfo === 'string'){ 1718 | $encoding = static::$env['mbStr'] ? mb_detect_encoding($key) : ''; 1719 | $length = $encoding && ($encoding !== 'ASCII') ? static::strLen($key) . '; ' . $encoding : static::strLen($key); 1720 | $keyInfo = sprintf('%s(%s)', $keyInfo, $length); 1721 | } 1722 | 1723 | $this->fmt->startRow(); 1724 | $this->fmt->text(array('key', 'iterator'), $key, sprintf('Iterator key: %s', $keyInfo)); 1725 | $this->fmt->colDiv(); 1726 | $this->fmt->sep('=>'); 1727 | $this->fmt->colDiv(); 1728 | $this->evaluate($value); 1729 | //$this->evaluate($value instanceof \Traversable ? ((count($value) > 0) ? $value : (string)$value) : $value); 1730 | $this->fmt->endRow(); 1731 | } 1732 | } 1733 | 1734 | // display the interfaces this objects' class implements 1735 | if($interfaces){ 1736 | $items = array(); 1737 | $this->fmt->sectionTitle('Implements'); 1738 | $this->fmt->startRow(); 1739 | $this->fmt->startContain('interfaces'); 1740 | 1741 | $i = 0; 1742 | $count = count($interfaces); 1743 | 1744 | foreach($interfaces as $name => $interface){ 1745 | $this->fromReflector($interface); 1746 | 1747 | if(++$i < $count) 1748 | $this->fmt->sep(', '); 1749 | } 1750 | 1751 | $this->fmt->endContain(); 1752 | $this->fmt->endRow(); 1753 | } 1754 | 1755 | // traits this objects' class uses 1756 | if($traits){ 1757 | $items = array(); 1758 | $this->fmt->sectionTitle('Uses'); 1759 | $this->fmt->startRow(); 1760 | $this->fmt->startContain('traits'); 1761 | 1762 | $i = 0; 1763 | $count = count($traits); 1764 | 1765 | foreach($traits as $name => $trait){ 1766 | $this->fromReflector($trait); 1767 | 1768 | if(++$i < $count) 1769 | $this->fmt->sep(', '); 1770 | } 1771 | 1772 | $this->fmt->endContain(); 1773 | $this->fmt->endRow(); 1774 | } 1775 | 1776 | // class constants 1777 | if($constants){ 1778 | $this->fmt->sectionTitle('Constants'); 1779 | $max = max(array_map('static::strLen', array_keys($constants))); 1780 | foreach($constants as $name => $value){ 1781 | $meta = null; 1782 | $type = array('const'); 1783 | foreach($parents as $parent){ 1784 | if($parent->hasConstant($name)){ 1785 | if($parent !== $reflector){ 1786 | $type[] = 'inherited'; 1787 | $meta = array('sub' => array(array('Prototype defined by', $parent->name))); 1788 | } 1789 | break; 1790 | } 1791 | } 1792 | 1793 | $this->fmt->startRow(); 1794 | $this->fmt->sep('::'); 1795 | $this->fmt->colDiv(); 1796 | $this->fmt->startContain($type); 1797 | $this->fmt->text('name', $name, $meta, $this->linkify($parent, $name)); 1798 | $this->fmt->endContain(); 1799 | $this->fmt->colDiv($max - static::strLen($name)); 1800 | $this->fmt->sep('='); 1801 | $this->fmt->colDiv(); 1802 | $this->evaluate($value); 1803 | $this->fmt->endRow(); 1804 | } 1805 | } 1806 | 1807 | // object/class properties 1808 | if($props){ 1809 | $this->fmt->sectionTitle('Properties'); 1810 | 1811 | $max = 0; 1812 | foreach($props as $idx => $prop) 1813 | if(($propNameLen = static::strLen($prop->name)) > $max) 1814 | $max = $propNameLen; 1815 | 1816 | foreach($props as $idx => $prop){ 1817 | 1818 | if($this->hasInstanceTimedOut()) 1819 | break; 1820 | 1821 | $bubbles = array(); 1822 | $sourceClass = $prop->getDeclaringClass(); 1823 | $inherited = $reflector->getShortName() !== $sourceClass->getShortName(); 1824 | $meta = $sourceClass->isInternal() ? null : static::parseComment($prop->getDocComment()); 1825 | 1826 | if($meta){ 1827 | if($inherited) 1828 | $meta['sub'] = array(array('Declared in', $sourceClass->getShortName())); 1829 | 1830 | if(isset($meta['tags']['var'][0])) 1831 | $meta['left'] = $meta['tags']['var'][0][0]; 1832 | 1833 | unset($meta['tags']); 1834 | } 1835 | 1836 | if($prop->isProtected() || $prop->isPrivate()) 1837 | $prop->setAccessible(true); 1838 | 1839 | $value = $prop->getValue($subject); 1840 | 1841 | $this->fmt->startRow(); 1842 | $this->fmt->sep($prop->isStatic() ? '::' : '->'); 1843 | $this->fmt->colDiv(); 1844 | 1845 | $bubbles = array(); 1846 | if($prop->isProtected()) 1847 | $bubbles[] = array('P', 'Protected'); 1848 | 1849 | if($prop->isPrivate()) 1850 | $bubbles[] = array('!', 'Private'); 1851 | 1852 | $this->fmt->bubbles($bubbles); 1853 | 1854 | $type = array('prop'); 1855 | 1856 | if($inherited) 1857 | $type[] = 'inherited'; 1858 | 1859 | if($prop->isPrivate()) 1860 | $type[] = 'private'; 1861 | 1862 | $this->fmt->colDiv(2 - count($bubbles)); 1863 | $this->fmt->startContain($type); 1864 | $this->fmt->text('name', $prop->name, $meta, $this->linkify($prop)); 1865 | $this->fmt->endContain(); 1866 | $this->fmt->colDiv($max - static::strLen($prop->name)); 1867 | $this->fmt->sep('='); 1868 | $this->fmt->colDiv(); 1869 | $this->evaluate($value); 1870 | $this->fmt->endRow(); 1871 | } 1872 | } 1873 | 1874 | // __debugInfo() 1875 | if($magicProps){ 1876 | $this->fmt->sectionTitle('Properties (magic)'); 1877 | 1878 | $max = 0; 1879 | foreach($magicProps as $name => $value) 1880 | if(($propNameLen = static::strLen($name)) > $max) 1881 | $max = $propNameLen; 1882 | 1883 | foreach($magicProps as $name => $value){ 1884 | 1885 | if($this->hasInstanceTimedOut()) 1886 | break; 1887 | 1888 | // attempt to pull out doc comment from the "regular" property definition 1889 | try{ 1890 | $prop = $reflector->getProperty($name); 1891 | $meta = static::parseComment($prop->getDocComment()); 1892 | 1893 | }catch(\Exception $e){ 1894 | $meta = null; 1895 | } 1896 | 1897 | $this->fmt->startRow(); 1898 | $this->fmt->sep('->'); 1899 | $this->fmt->colDiv(); 1900 | 1901 | $type = array('prop'); 1902 | 1903 | $this->fmt->startContain($type); 1904 | $this->fmt->text('name', $name, $meta); 1905 | $this->fmt->endContain(); 1906 | $this->fmt->colDiv($max - static::strLen($name)); 1907 | $this->fmt->sep('='); 1908 | $this->fmt->colDiv(); 1909 | $this->evaluate($value); 1910 | $this->fmt->endRow(); 1911 | } 1912 | } 1913 | 1914 | // class methods 1915 | if($methods && !$this->hasInstanceTimedOut()){ 1916 | 1917 | $this->fmt->sectionTitle('Methods'); 1918 | foreach($methods as $idx => $method){ 1919 | 1920 | $this->fmt->startRow(); 1921 | $this->fmt->sep($method->isStatic() ? '::' : '->'); 1922 | $this->fmt->colDiv(); 1923 | 1924 | $bubbles = array(); 1925 | if($method->isAbstract()) 1926 | $bubbles[] = array('A', 'Abstract'); 1927 | 1928 | if($method->isFinal()) 1929 | $bubbles[] = array('F', 'Final'); 1930 | 1931 | if($method->isProtected()) 1932 | $bubbles[] = array('P', 'Protected'); 1933 | 1934 | if($method->isPrivate()) 1935 | $bubbles[] = array('!', 'Private'); 1936 | 1937 | $this->fmt->bubbles($bubbles); 1938 | 1939 | $this->fmt->colDiv(4 - count($bubbles)); 1940 | 1941 | // is this method inherited? 1942 | $inherited = $reflector->getShortName() !== $method->getDeclaringClass()->getShortName(); 1943 | 1944 | $type = array('method'); 1945 | 1946 | if($inherited) 1947 | $type[] = 'inherited'; 1948 | 1949 | if($method->isPrivate()) 1950 | $type[] = 'private'; 1951 | 1952 | $this->fmt->startContain($type); 1953 | 1954 | $name = $method->name; 1955 | if($method->returnsReference()) 1956 | $name = "&{$name}"; 1957 | 1958 | $this->fromReflector($method, $name, $reflector); 1959 | 1960 | $paramCom = $method->isInternal() ? array() : static::parseComment($method->getDocComment(), 'tags'); 1961 | $paramCom = empty($paramCom['param']) ? array() : $paramCom['param']; 1962 | $paramCount = $method->getNumberOfParameters(); 1963 | 1964 | $this->fmt->sep('('); 1965 | 1966 | // process arguments 1967 | foreach($method->getParameters() as $idx => $parameter){ 1968 | $meta = null; 1969 | $paramName = "\${$parameter->name}"; 1970 | $optional = $parameter->isOptional(); 1971 | $variadic = static::$env['is56'] && $parameter->isVariadic(); 1972 | 1973 | if($parameter->isPassedByReference()) 1974 | $paramName = "&{$paramName}"; 1975 | 1976 | if($variadic) 1977 | $paramName = "...{$paramName}"; 1978 | 1979 | $type = array('param'); 1980 | 1981 | if($optional) 1982 | $type[] = 'optional'; 1983 | 1984 | $this->fmt->startContain($type); 1985 | 1986 | // attempt to build meta 1987 | foreach($paramCom as $tag){ 1988 | list($pcTypes, $pcName, $pcDescription) = $tag; 1989 | if($pcName !== $paramName) 1990 | continue; 1991 | 1992 | $meta = array('title' => $pcDescription); 1993 | 1994 | if($pcTypes) 1995 | $meta['left'] = $pcTypes; 1996 | 1997 | break; 1998 | } 1999 | 2000 | try{ 2001 | $paramClass = $parameter->getClass(); 2002 | }catch(\Exception $e){ 2003 | // @see https://bugs.php.net/bug.php?id=32177&edit=1 2004 | } 2005 | 2006 | if(!empty($paramClass)){ 2007 | $this->fmt->startContain('hint'); 2008 | $this->fromReflector($paramClass, $paramClass->name); 2009 | $this->fmt->endContain(); 2010 | $this->fmt->sep(' '); 2011 | 2012 | }elseif($parameter->isArray()){ 2013 | $this->fmt->text('hint', 'array'); 2014 | $this->fmt->sep(' '); 2015 | 2016 | }else{ 2017 | $hasType = static::$env['is7'] && $parameter->hasType(); 2018 | if($hasType){ 2019 | $type = $parameter->getType(); 2020 | $this->fmt->text('hint', (string)$type); 2021 | $this->fmt->sep(' '); 2022 | } 2023 | } 2024 | 2025 | $this->fmt->text('name', $paramName, $meta); 2026 | 2027 | if($optional){ 2028 | try{ 2029 | $paramValue = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; 2030 | if($paramValue !== null){ 2031 | $this->fmt->sep(' = '); 2032 | 2033 | if(static::$env['is546'] && !$parameter->getDeclaringFunction()->isInternal() && $parameter->isDefaultValueConstant()){ 2034 | $this->fmt->text('constant', $parameter->getDefaultValueConstantName(), 'Constant'); 2035 | 2036 | }else{ 2037 | $this->evaluate($paramValue, true); 2038 | } 2039 | } 2040 | 2041 | }catch(\Exception $e){ 2042 | // unable to retrieve default value? 2043 | } 2044 | 2045 | } 2046 | 2047 | $this->fmt->endContain(); 2048 | 2049 | if($idx < $paramCount - 1) 2050 | $this->fmt->sep(', '); 2051 | } 2052 | $this->fmt->sep(')'); 2053 | $this->fmt->endContain(); 2054 | 2055 | $hasReturnType = static::$env['is7'] && $method->hasReturnType(); 2056 | if($hasReturnType){ 2057 | $type = $method->getReturnType(); 2058 | $this->fmt->startContain('ret'); 2059 | $this->fmt->sep(':'); 2060 | $this->fmt->text('hint', (string)$type); 2061 | $this->fmt->endContain(); 2062 | } 2063 | 2064 | $this->fmt->endRow(); 2065 | } 2066 | } 2067 | 2068 | unset($hashes[$hash]); 2069 | $this->fmt->endGroup(); 2070 | 2071 | $this->fmt->cacheLock($hash); 2072 | } 2073 | 2074 | 2075 | 2076 | /** 2077 | * Scans for known classes and functions inside the provided expression, 2078 | * and linkifies them when possible 2079 | * 2080 | * @param string $expression Expression to format 2081 | * @return string Formatted output 2082 | */ 2083 | protected function evaluateExp($expression = null){ 2084 | 2085 | if($expression === null) 2086 | return; 2087 | 2088 | if(static::strLen($expression) > 120) 2089 | $expression = substr($expression, 0, 120) . '...'; 2090 | 2091 | $this->fmt->sep('> '); 2092 | 2093 | if(strpos($expression, '(') === false) 2094 | return $this->fmt->text('expTxt', $expression); 2095 | 2096 | $keywords = array_map('trim', explode('(', $expression, 2)); 2097 | $parts = array(); 2098 | 2099 | // try to find out if this is a function 2100 | try{ 2101 | $reflector = new \ReflectionFunction($keywords[0]); 2102 | $parts[] = array($keywords[0], $reflector, ''); 2103 | 2104 | }catch(\Exception $e){ 2105 | 2106 | if(stripos($keywords[0], 'new ') === 0){ 2107 | $cn = explode(' ' , $keywords[0], 2); 2108 | 2109 | // linkify 'new keyword' (as constructor) 2110 | try{ 2111 | $reflector = new \ReflectionMethod($cn[1], '__construct'); 2112 | $parts[] = array($cn[0], $reflector, ''); 2113 | 2114 | }catch(\Exception $e){ 2115 | $reflector = null; 2116 | $parts[] = $cn[0]; 2117 | } 2118 | 2119 | // class name... 2120 | try{ 2121 | $reflector = new \ReflectionClass($cn[1]); 2122 | $parts[] = array($cn[1], $reflector, ' '); 2123 | 2124 | }catch(\Exception $e){ 2125 | $reflector = null; 2126 | $parts[] = $cn[1]; 2127 | } 2128 | 2129 | }else{ 2130 | 2131 | // we can only linkify methods called statically 2132 | if(strpos($keywords[0], '::') === false) 2133 | return $this->fmt->text('expTxt', $expression); 2134 | 2135 | $cn = explode('::', $keywords[0], 2); 2136 | 2137 | // attempt to linkify class name 2138 | try{ 2139 | $reflector = new \ReflectionClass($cn[0]); 2140 | $parts[] = array($cn[0], $reflector, ''); 2141 | 2142 | }catch(\Exception $e){ 2143 | $reflector = null; 2144 | $parts[] = $cn[0]; 2145 | } 2146 | 2147 | // perhaps it's a static class method; try to linkify method 2148 | try{ 2149 | $reflector = new \ReflectionMethod($cn[0], $cn[1]); 2150 | $parts[] = array($cn[1], $reflector, '::'); 2151 | 2152 | }catch(\Exception $e){ 2153 | $reflector = null; 2154 | $parts[] = $cn[1]; 2155 | } 2156 | } 2157 | } 2158 | 2159 | $parts[] = "({$keywords[1]}"; 2160 | 2161 | foreach($parts as $element){ 2162 | if(!is_array($element)){ 2163 | $this->fmt->text('expTxt', $element); 2164 | continue; 2165 | } 2166 | 2167 | list($text, $reflector, $prefix) = $element; 2168 | 2169 | if($prefix !== '') 2170 | $this->fmt->text('expTxt', $prefix); 2171 | 2172 | $this->fromReflector($reflector, $text); 2173 | } 2174 | 2175 | } 2176 | 2177 | 2178 | 2179 | /** 2180 | * Calculates real string length 2181 | * 2182 | * @param string $string 2183 | * @return int 2184 | */ 2185 | protected static function strLen($string){ 2186 | $encoding = function_exists('mb_detect_encoding') ? mb_detect_encoding($string) : false; 2187 | return $encoding ? mb_strlen($string, $encoding) : strlen($string); 2188 | } 2189 | 2190 | 2191 | 2192 | /** 2193 | * Safe str_pad alternative 2194 | * 2195 | * @param string $string 2196 | * @param int $padLen 2197 | * @param string $padStr 2198 | * @param int $padType 2199 | * @return string 2200 | */ 2201 | protected static function strPad($input, $padLen, $padStr = ' ', $padType = STR_PAD_RIGHT){ 2202 | $diff = strlen($input) - static::strLen($input); 2203 | return str_pad($input, $padLen + $diff, $padStr, $padType); 2204 | } 2205 | 2206 | } 2207 | 2208 | 2209 | 2210 | /** 2211 | * Formatter abstraction 2212 | */ 2213 | abstract class RFormatter{ 2214 | 2215 | /** 2216 | * Flush output and send contents to the output device 2217 | */ 2218 | abstract public function flush(); 2219 | 2220 | /** 2221 | * Generate a base entity 2222 | * 2223 | * @param string|array $type 2224 | * @param string|null $text 2225 | * @param string|array|null $meta 2226 | * @param string|null $uri 2227 | */ 2228 | abstract public function text($type, $text = null, $meta = null, $uri = null); 2229 | 2230 | /** 2231 | * Generate container start token 2232 | * 2233 | * @param string|array $type 2234 | * @param string|bool $label 2235 | */ 2236 | public function startContain($type, $label = false){} 2237 | 2238 | /** 2239 | * Generate container ending token 2240 | */ 2241 | public function endContain(){} 2242 | 2243 | /** 2244 | * Generate empty group token 2245 | * 2246 | * @param string $prefix 2247 | */ 2248 | public function emptyGroup($prefix = ''){} 2249 | 2250 | /** 2251 | * Generate group start token 2252 | * 2253 | * This method must return boolean TRUE on success, false otherwise (eg. max depth reached). 2254 | * The evaluator will skip this group on FALSE 2255 | * 2256 | * @param string $prefix 2257 | * @return bool 2258 | */ 2259 | public function startGroup($prefix = ''){} 2260 | 2261 | /** 2262 | * Generate group ending token 2263 | */ 2264 | public function endGroup(){} 2265 | 2266 | /** 2267 | * Generate section title 2268 | * 2269 | * @param string $title 2270 | */ 2271 | public function sectionTitle($title){} 2272 | 2273 | /** 2274 | * Generate row start token 2275 | */ 2276 | public function startRow(){} 2277 | 2278 | /** 2279 | * Generate row ending token 2280 | */ 2281 | public function endRow(){} 2282 | 2283 | /** 2284 | * Column divider (cell delimiter) 2285 | * 2286 | * @param int $padLen 2287 | */ 2288 | public function colDiv($padLen = null){} 2289 | 2290 | /** 2291 | * Generate modifier tokens 2292 | * 2293 | * @param array $items 2294 | */ 2295 | public function bubbles(array $items){} 2296 | 2297 | /** 2298 | * Input expression start 2299 | */ 2300 | public function startExp(){} 2301 | 2302 | /** 2303 | * Input expression end 2304 | */ 2305 | public function endExp(){} 2306 | 2307 | /** 2308 | * Root starting token 2309 | */ 2310 | public function startRoot(){} 2311 | 2312 | /** 2313 | * Root ending token 2314 | */ 2315 | public function endRoot(){} 2316 | 2317 | /** 2318 | * Separator token 2319 | * 2320 | * @param string $label 2321 | */ 2322 | public function sep($label = ' '){} 2323 | 2324 | /** 2325 | * Resolve cache request 2326 | * 2327 | * If the ID is not present in the cache, then a new cache entry is created 2328 | * for the given ID, and string offsets are captured until cacheLock is called 2329 | * 2330 | * This method must return TRUE if the ID exists in the cache, and append the cached item 2331 | * to the output, FALSE otherwise. 2332 | * 2333 | * @param string $id 2334 | * @return bool 2335 | */ 2336 | public function didCache($id){ 2337 | return false; 2338 | } 2339 | 2340 | /** 2341 | * Ends cache capturing for the given ID 2342 | * 2343 | * @param string $id 2344 | */ 2345 | public function cacheLock($id){} 2346 | 2347 | } 2348 | 2349 | 2350 | 2351 | 2352 | /** 2353 | * Generates the output in HTML5 format 2354 | * 2355 | */ 2356 | class RHtmlFormatter extends RFormatter{ 2357 | 2358 | protected 2359 | 2360 | /** 2361 | * Actual output 2362 | * 2363 | * @var string 2364 | */ 2365 | $out = '', 2366 | 2367 | /** 2368 | * Tracks current nesting level 2369 | * 2370 | * @var int 2371 | */ 2372 | $level = 0, 2373 | 2374 | /** 2375 | * Stores tooltip content for all entries 2376 | * 2377 | * To avoid having duplicate tooltip data in the HTML, we generate them once, 2378 | * and use references (the Q index) to pull data when required; 2379 | * this improves performance significantly 2380 | * 2381 | * @var array 2382 | */ 2383 | $tips = array(), 2384 | 2385 | /** 2386 | * Used to cache output to speed up processing. 2387 | * 2388 | * Contains hashes as keys and string offsets as values. 2389 | * Cached objects will not be processed again in the same query 2390 | * 2391 | * @var array 2392 | */ 2393 | $cache = array(), 2394 | 2395 | /** 2396 | * Map of used HTML tag and attributes 2397 | * 2398 | * @var string 2399 | */ 2400 | $def = array(); 2401 | 2402 | 2403 | 2404 | protected static 2405 | 2406 | /** 2407 | * Instance counter 2408 | * 2409 | * @var int 2410 | */ 2411 | $counter = 0, 2412 | 2413 | /** 2414 | * Tracks style/jscript inclusion state 2415 | * 2416 | * @var bool 2417 | */ 2418 | $didAssets = false; 2419 | 2420 | 2421 | public function __construct(){ 2422 | 2423 | if(ref::config('validHtml')){ 2424 | 2425 | $this->def = array( 2426 | 'base' => 'span', 2427 | 'tip' => 'div', 2428 | 'cell' => 'data-cell', 2429 | 'table' => 'data-table', 2430 | 'row' => 'data-row', 2431 | 'group' => 'data-group', 2432 | 'gLabel' => 'data-gLabel', 2433 | 'match' => 'data-match', 2434 | 'tipRef' => 'data-tip', 2435 | ); 2436 | 2437 | 2438 | }else{ 2439 | 2440 | $this->def = array( 2441 | 'base' => 'r', 2442 | 'tip' => 't', 2443 | 'cell' => 'c', 2444 | 'table' => 't', 2445 | 'row' => 'r', 2446 | 'group' => 'g', 2447 | 'gLabel' => 'gl', 2448 | 'match' => 'm', 2449 | 'tipRef' => 'h', 2450 | ); 2451 | 2452 | } 2453 | 2454 | } 2455 | 2456 | 2457 | 2458 | public function flush(){ 2459 | print $this->out; 2460 | $this->out = ''; 2461 | $this->cache = array(); 2462 | $this->tips = array(); 2463 | } 2464 | 2465 | 2466 | public function didCache($id){ 2467 | 2468 | if(!isset($this->cache[$id])){ 2469 | $this->cache[$id] = array(); 2470 | $this->cache[$id][] = strlen($this->out); 2471 | return false; 2472 | } 2473 | 2474 | if(!isset($this->cache[$id][1])){ 2475 | $this->cache[$id][0] = strlen($this->out); 2476 | return false; 2477 | } 2478 | 2479 | $this->out .= substr($this->out, $this->cache[$id][0], $this->cache[$id][1]); 2480 | return true; 2481 | } 2482 | 2483 | public function cacheLock($id){ 2484 | $this->cache[$id][] = strlen($this->out) - $this->cache[$id][0]; 2485 | } 2486 | 2487 | 2488 | public function sep($label = ' '){ 2489 | $this->out .= $label !== ' ' ? '' . static::escape($label) . '' : $label; 2490 | } 2491 | 2492 | public function text($type, $text = null, $meta = null, $uri = null){ 2493 | 2494 | if(!is_array($type)) 2495 | $type = (array)$type; 2496 | 2497 | $tip = ''; 2498 | $text = ($text !== null) ? static::escape($text) : static::escape($type[0]); 2499 | 2500 | if(in_array('special', $type)){ 2501 | $text = strtr($text, array( 2502 | "\r" => '\r', // carriage return 2503 | "\t" => '\t', // horizontal tab 2504 | "\n" => '\n', // linefeed (new line) 2505 | "\v" => '\v', // vertical tab 2506 | "\e" => '\e', // escape 2507 | "\f" => '\f', // form feed 2508 | "\0" => '\0', 2509 | )); 2510 | } 2511 | 2512 | // generate tooltip reference (probably the slowest part of the code ;) 2513 | if($meta !== null){ 2514 | $tipIdx = array_search($meta, $this->tips, true); 2515 | 2516 | if($tipIdx === false) 2517 | $tipIdx = array_push($this->tips, $meta) - 1; 2518 | 2519 | $tip = " {$this->def['tipRef']}=\"{$tipIdx}\""; 2520 | //$tip = sprintf('%s="%d"', $this->def['tipRef'], $tipIdx); 2521 | } 2522 | 2523 | // wrap text in a link? 2524 | if($uri !== null) 2525 | $text = '' . $text . ''; 2526 | 2527 | $typeStr = ''; 2528 | foreach($type as $part) 2529 | $typeStr .= " data-{$part}"; 2530 | 2531 | $this->out .= "<{$this->def['base']}{$typeStr}{$tip}>{$text}def['base']}>"; 2532 | //$this->out .= sprintf('<%1$s%2$s %3$s>%4$s', $this->def['base'], $typeStr, $tip, $text); 2533 | } 2534 | 2535 | public function startContain($type, $label = false){ 2536 | 2537 | if(!is_array($type)) 2538 | $type = (array)$type; 2539 | 2540 | if($label) 2541 | $this->out .= '
'; 2542 | 2543 | $typeStr = ''; 2544 | foreach($type as $part) 2545 | $typeStr .= " data-{$part}"; 2546 | 2547 | $this->out .= "<{$this->def['base']}{$typeStr}>"; 2548 | 2549 | if($label) 2550 | $this->out .= "<{$this->def['base']} {$this->def['match']}>{$type[0]}def['base']}>"; 2551 | } 2552 | 2553 | public function endContain(){ 2554 | $this->out .= "def['base']}>"; 2555 | } 2556 | 2557 | public function emptyGroup($prefix = ''){ 2558 | 2559 | if($prefix !== '') 2560 | $prefix = "<{$this->def['base']} {$this->def['gLabel']}>" . static::escape($prefix) . "def['base']}>"; 2561 | 2562 | $this->out .= "({$prefix})"; 2563 | } 2564 | 2565 | 2566 | public function startGroup($prefix = ''){ 2567 | 2568 | $maxDepth = ref::config('maxDepth'); 2569 | 2570 | if(($maxDepth > 0) && (($this->level + 1) > $maxDepth)){ 2571 | $this->emptyGroup('...'); 2572 | return false; 2573 | } 2574 | 2575 | $this->level++; 2576 | 2577 | $expLvl = ref::config('expLvl'); 2578 | $exp = ($expLvl < 0) || (($expLvl > 0) && ($this->level <= $expLvl)) ? ' data-exp' : ''; 2579 | 2580 | if($prefix !== '') 2581 | $prefix = "<{$this->def['base']} {$this->def['gLabel']}>" . static::escape($prefix) . "def['base']}>"; 2582 | 2583 | $this->out .= "({$prefix}<{$this->def['base']} data-toggle{$exp}>def['base']}><{$this->def['base']} {$this->def['group']}><{$this->def['base']} {$this->def['table']}>"; 2584 | 2585 | return true; 2586 | } 2587 | 2588 | public function endGroup(){ 2589 | $this->out .= "def['base']}>def['base']}>)"; 2590 | $this->level--; 2591 | } 2592 | 2593 | public function sectionTitle($title){ 2594 | $this->out .= "def['base']}><{$this->def['base']} data-tHead>{$title}def['base']}><{$this->def['base']} {$this->def['table']}>"; 2595 | } 2596 | 2597 | public function startRow(){ 2598 | $this->out .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>"; 2599 | } 2600 | 2601 | public function endRow(){ 2602 | $this->out .= "def['base']}>def['base']}>"; 2603 | } 2604 | 2605 | public function colDiv($padLen = null){ 2606 | $this->out .= "def['base']}><{$this->def['base']} {$this->def['cell']}>"; 2607 | } 2608 | 2609 | public function bubbles(array $items){ 2610 | 2611 | if(!$items) 2612 | return; 2613 | 2614 | $this->out .= "<{$this->def['base']} data-mod>"; 2615 | 2616 | foreach($items as $info) 2617 | $this->out .= $this->text('mod-' . strtolower($info[1]), $info[0], $info[1]); 2618 | 2619 | $this->out .= "def['base']}>"; 2620 | } 2621 | 2622 | public function startExp(){ 2623 | $this->out .= "<{$this->def['base']} data-input>"; 2624 | } 2625 | 2626 | public function endExp(){ 2627 | if(ref::config('showBacktrace') && ($trace = ref::getBacktrace())){ 2628 | $docRoot = isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : ''; 2629 | $path = strpos($trace['file'], $docRoot) !== 0 ? $trace['file'] : ltrim(str_replace($docRoot, '', $trace['file']), '/'); 2630 | $this->out .= "<{$this->def['base']} data-backtrace>{$path}:{$trace['line']}def['base']}>"; 2631 | } 2632 | 2633 | $this->out .= "def['base']}><{$this->def['base']} data-output>"; 2634 | } 2635 | 2636 | public function startRoot(){ 2637 | $this->out .= '
' . static::getAssets() . '
'; 2638 | } 2639 | 2640 | public function endRoot(){ 2641 | $this->out .= "def['base']}>"; 2642 | 2643 | // process tooltips 2644 | $tipHtml = ''; 2645 | foreach($this->tips as $idx => $meta){ 2646 | 2647 | $tip = ''; 2648 | if(!is_array($meta)) 2649 | $meta = array('title' => $meta); 2650 | 2651 | $meta += array( 2652 | 'title' => '', 2653 | 'left' => '', 2654 | 'description' => '', 2655 | 'tags' => array(), 2656 | 'sub' => array(), 2657 | ); 2658 | 2659 | $meta = static::escape($meta); 2660 | $cols = array(); 2661 | 2662 | if($meta['left']) 2663 | $cols[] = "<{$this->def['base']} {$this->def['cell']} data-varType>{$meta['left']}def['base']}>"; 2664 | 2665 | $title = $meta['title'] ? "<{$this->def['base']} data-title>{$meta['title']}def['base']}>" : ''; 2666 | $desc = $meta['description'] ? "<{$this->def['base']} data-desc>{$meta['description']}def['base']}>" : ''; 2667 | $tags = ''; 2668 | 2669 | foreach($meta['tags'] as $tag => $values){ 2670 | foreach($values as $value){ 2671 | if($tag === 'param'){ 2672 | $value[0] = "{$value[0]} {$value[1]}"; 2673 | unset($value[1]); 2674 | } 2675 | 2676 | $value = is_array($value) ? implode("def['base']}><{$this->def['base']} {$this->def['cell']}>", $value) : $value; 2677 | $tags .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>@{$tag}def['base']}><{$this->def['base']} {$this->def['cell']}>{$value}def['base']}>def['base']}>"; 2678 | } 2679 | } 2680 | 2681 | if($tags) 2682 | $tags = "<{$this->def['base']} {$this->def['table']}>{$tags}def['base']}>"; 2683 | 2684 | if($title || $desc || $tags) 2685 | $cols[] = "<{$this->def['base']} {$this->def['cell']}>{$title}{$desc}{$tags}def['base']}>"; 2686 | 2687 | if($cols) 2688 | $tip = "<{$this->def['base']} {$this->def['row']}>" . implode('', $cols) . "def['base']}>"; 2689 | 2690 | $sub = ''; 2691 | foreach($meta['sub'] as $line) 2692 | $sub .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>" . implode("def['base']}><{$this->def['base']} {$this->def['cell']}>", $line) . "def['base']}>def['base']}>"; 2693 | 2694 | if($sub) 2695 | $tip .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']} data-sub><{$this->def['base']} {$this->def['table']}>{$sub}def['base']}>def['base']}>def['base']}>"; 2696 | 2697 | if($tip) 2698 | $this->out .= "<{$this->def['tip']}>{$tip}def['tip']}>"; 2699 | } 2700 | 2701 | if(($timeout = ref::getTimeoutPoint()) > 0) 2702 | $this->out .= sprintf("<{$this->def['base']} data-error>Listing incomplete. Timed-out after %4.2fsdef['base']}>", $timeout); 2703 | 2704 | $this->out .= '
'; 2705 | } 2706 | 2707 | 2708 | 2709 | /** 2710 | * Get styles and javascript (only generated for the 1st call) 2711 | * 2712 | * @return string 2713 | */ 2714 | public static function getAssets(){ 2715 | 2716 | // first call? include styles and javascript 2717 | if(static::$didAssets) 2718 | return ''; 2719 | 2720 | ob_start(); 2721 | 2722 | if(ref::config('stylePath') !== false){ 2723 | ?> 2724 | 2727 | 2732 | 2735 | out; 2799 | $this->out = ''; 2800 | $this->cache = array(); 2801 | } 2802 | 2803 | public function sep($label = ' '){ 2804 | $this->out .= $label; 2805 | } 2806 | 2807 | public function text($type, $text = null, $meta = null, $uri = null){ 2808 | 2809 | if(!is_array($type)) 2810 | $type = (array)$type; 2811 | 2812 | if($text === null) 2813 | $text = $type[0]; 2814 | 2815 | if(in_array('special', $type, true)){ 2816 | $text = strtr($text, array( 2817 | "\r" => '\r', // carriage return 2818 | "\t" => '\t', // horizontal tab 2819 | "\n" => '\n', // linefeed (new line) 2820 | "\v" => '\v', // vertical tab 2821 | "\e" => '\e', // escape 2822 | "\f" => '\f', // form feed 2823 | "\0" => '\0', 2824 | )); 2825 | 2826 | $this->out .= $text; 2827 | return; 2828 | } 2829 | 2830 | $formatMap = array( 2831 | 'string' => '%3$s "%2$s"', 2832 | 'integer' => 'int(%2$s)', 2833 | 'double' => 'double(%2$s)', 2834 | 'true' => 'bool(%2$s)', 2835 | 'false' => 'bool(%2$s)', 2836 | 'key' => '[%2$s]', 2837 | ); 2838 | 2839 | if(!is_string($meta)) 2840 | $meta = ''; 2841 | 2842 | $this->out .= isset($formatMap[$type[0]]) ? sprintf($formatMap[$type[0]], $type[0], $text, $meta) : $text; 2843 | } 2844 | 2845 | public function startContain($type, $label = false){ 2846 | 2847 | if(!is_array($type)) 2848 | $type = (array)$type; 2849 | 2850 | if($label) 2851 | $this->out .= "\n" . str_repeat(' ', $this->indent + $this->levelPad[$this->level]) . "┗ {$type[0]} ~ "; 2852 | } 2853 | 2854 | public function emptyGroup($prefix = ''){ 2855 | $this->out .= "({$prefix})"; 2856 | } 2857 | 2858 | public function startGroup($prefix = ''){ 2859 | 2860 | $maxDepth = ref::config('maxDepth'); 2861 | 2862 | if(($maxDepth > 0) && (($this->level + 1) > $maxDepth)){ 2863 | $this->emptyGroup('...'); 2864 | return false; 2865 | } 2866 | 2867 | $this->level++; 2868 | $this->out .= '('; 2869 | 2870 | $this->indent += $this->levelPad[$this->level - 1]; 2871 | return true; 2872 | } 2873 | 2874 | public function endGroup(){ 2875 | $this->out .= "\n" . str_repeat(' ', $this->indent) . ')'; 2876 | $this->indent -= $this->levelPad[$this->level - 1]; 2877 | $this->level--; 2878 | } 2879 | 2880 | public function sectionTitle($title){ 2881 | $pad = str_repeat(' ', $this->indent + 2); 2882 | $this->out .= sprintf("\n\n%s%s\n%s%s", $pad, $title, $pad, str_repeat('-', strlen($title))); 2883 | } 2884 | 2885 | public function startRow(){ 2886 | $this->out .= "\n " . str_repeat(' ', $this->indent); 2887 | $this->lastLineSt = strlen($this->out); 2888 | } 2889 | 2890 | public function endRow(){ 2891 | } 2892 | 2893 | public function colDiv($padLen = null){ 2894 | $padLen = ($padLen !== null) ? $padLen + 1 : 1; 2895 | $this->out .= str_repeat(' ', $padLen); 2896 | 2897 | $this->lastIdx = strlen($this->out); 2898 | $this->levelPad[$this->level] = $this->lastIdx - $this->lastLineSt + 2; 2899 | } 2900 | 2901 | public function bubbles(array $items){ 2902 | 2903 | if(!$items){ 2904 | $this->out .= ' '; 2905 | return; 2906 | } 2907 | 2908 | $this->out .= '<'; 2909 | 2910 | foreach($items as $item) 2911 | $this->out .= $item[0]; 2912 | 2913 | $this->out .= '>'; 2914 | } 2915 | 2916 | public function endExp(){ 2917 | 2918 | if(ref::config('showBacktrace') && ($trace = ref::getBacktrace())) 2919 | $this->out .= ' - ' . $trace['file'] . ':' . $trace['line']; 2920 | 2921 | $this->out .= "\n" . str_repeat('=', strlen($this->out)) . "\n"; 2922 | } 2923 | 2924 | public function startRoot(){ 2925 | $this->out .= "\n\n"; 2926 | 2927 | } 2928 | 2929 | public function endRoot(){ 2930 | $this->out .= "\n"; 2931 | if(($timeout = ref::getTimeoutPoint()) > 0) 2932 | $this->out .= sprintf("\n-- Listing incomplete. Timed-out after %4.2fs -- \n", $timeout); 2933 | } 2934 | 2935 | } 2936 | 2937 | 2938 | 2939 | /** 2940 | * Text formatter with color support for CLI -- unfinished 2941 | * 2942 | */ 2943 | class RCliTextFormatter extends RTextFormatter{ 2944 | 2945 | public function sectionTitle($title){ 2946 | $pad = str_repeat(' ', $this->indent + 2); 2947 | $this->out .= sprintf("\n\n%s\x1b[4;97m%s\x1b[0m", $pad, $title); 2948 | } 2949 | 2950 | public function startExp(){ 2951 | $this->out .= "\x1b[1;44;96m "; 2952 | } 2953 | 2954 | public function endExp(){ 2955 | if(ref::config('showBacktrace') && ($trace = ref::getBacktrace())) 2956 | $this->out .= "\x1b[0m\x1b[44;36m " . $trace['file'] . ':' . $trace['line']; 2957 | 2958 | $this->out .= " \x1b[0m\n"; 2959 | } 2960 | 2961 | public function endRoot(){ 2962 | $this->out .= "\n"; 2963 | if(($timeout = ref::getTimeoutPoint()) > 0) 2964 | $this->out .= sprintf("\n\x1b[3;91m-- Listing incomplete. Timed-out after %4.2fs --\x1b[0m\n", $timeout); 2965 | } 2966 | 2967 | } 2968 | -------------------------------------------------------------------------------- /tests/example.class.php: -------------------------------------------------------------------------------- 1 | stuff = $list; 188 | $this->pubVarB = $this; 189 | $this->currentDate = \DateTime::createFromFormat('U', time(), new \DateTimeZone('Europe/London')); 190 | 191 | if(extension_loaded('gd')) 192 | $this->image = imagecreate(1, 1); 193 | 194 | if(extension_loaded('curl')){ 195 | $curl = curl_init(); 196 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); 197 | curl_setopt($curl, CURLOPT_URL, 'http://localhost'); 198 | curl_setopt($curl, CURLOPT_HEADER, 0); 199 | curl_exec($curl); 200 | 201 | $this->curl = $curl; 202 | } 203 | 204 | $this->jsonString = json_encode($this->currentDate); 205 | } 206 | 207 | 208 | 209 | /** 210 | * The destructor destroys the created image resource and the curl connection 211 | * 212 | * @since 1.0 213 | */ 214 | public function __destruct(){ 215 | 216 | if(isset($this->curl)) 217 | curl_close($this->curl); 218 | 219 | if(isset($this->image)) 220 | imagedestroy($this->image); 221 | } 222 | 223 | 224 | 225 | /** 226 | * A private method 227 | * 228 | * @since 1.0 229 | * @return array Normalized list 230 | */ 231 | private function normalizeList(){} 232 | 233 | 234 | 235 | /** 236 | * A public getter method 237 | * 238 | * @since 1.0 239 | * @return array Indexed array containing list items 240 | */ 241 | public function getList(ClassTest $x = null, $regexToIgnore = "#special\tabc\n#", $const = self::BAR){} 242 | 243 | 244 | 245 | /** 246 | * A protected setter method that returns a reference 247 | * 248 | * Accessible only from classes that extend this class 249 | * or from parent classes 250 | * 251 | * @since 1.0 252 | * @param array $list List as indexed array 253 | */ 254 | final protected function &setList(array $list){} 255 | 256 | 257 | 258 | /** 259 | * A static method that creates a new instance 260 | * 261 | * @since 1.0 262 | * @param array $list Indexed array containing list items 263 | * @return static A new instance of this class 264 | */ 265 | final public static function factory(array $list){} 266 | 267 | 268 | 269 | /** 270 | * A method that overrides parent::rewind() 271 | * 272 | * @since 1.0 273 | */ 274 | public function rewind(){ 275 | 276 | } 277 | 278 | } 279 | 280 | -------------------------------------------------------------------------------- /tests/file.txt: -------------------------------------------------------------------------------- 1 | test 2 | test 3 | test 4 | test 5 | test 6 | test 7 | test 8 | test 9 | test 10 | test 11 | test 12 | test 13 | test 14 | test 15 | test 16 | test 17 | test 18 | test 19 | test 20 | test 21 | test 22 | test 23 | test 24 | test 25 | test 26 | test 27 | test 28 | test 29 | test 30 | test 31 | test 32 | test 33 | test 34 | test 35 | test 36 | test 37 | test 38 | test 39 | test 40 | test 41 | test 42 | test 43 | test 44 | test 45 | test 46 | -------------------------------------------------------------------------------- /tests/index.php: -------------------------------------------------------------------------------- 1 | '(͡°͜ʖ͡°)', 39 | 'empty string' => '', 40 | 'multiline string' => "first line and some padding \nsecond line", 41 | 'infinity' => INF, 42 | 'regular expression (pcre)' => '/^([0-9a-zA-Z]([-\.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$/', 43 | 'multi' => array(1, 2, 3, array(4, 5, 6), 'FUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU'), 44 | 'matching class' => 'DateTime', 45 | 'matching file' => 'file.txt', 46 | 'incomplete object' => unserialize('O:3:"Foo":1:{s:3:"bar";i:5;}'), 47 | 'empty object' => new \StdClass(), 48 | 'closed CURL resource' => $closedCurlRes, 49 | 'matching date/file/function/class' => 'today', 50 | 'url' => 'http://google.com', 51 | ); 52 | 53 | $array['reference to self'] = &$array; 54 | 55 | $obj = new \Tests\ClassTest(array('foo', 'bar'), $array); 56 | 57 | if($htmlMode){ 58 | r(true, false, 'I can haz a 강남스타일 string', '1492-10-14 04:20:00 America/Nassau', null, 4.20); 59 | r(array(), $array, serialize(array('A', 'serialized', 'string'))); 60 | r(fopen('php://stdin', 'r'), function($x, $d){}); 61 | r(new \DateTimeZone('Pacific/Honolulu')); 62 | r($obj, new ref()); 63 | 64 | }else{ 65 | 66 | rt(true, false, 'I can haz a 강남스타일 string', '1492-10-14 04:20:00 America/Nassau', null, 17, 4.20); 67 | rt(array(), $array, serialize(array('A', 'serialized', 'string'))); 68 | rt(fopen('php://stdin', 'r'), function($x, $d){}); 69 | rt(new \DateTimeZone('Pacific/Honolulu')); 70 | rt($obj, new ref()); 71 | 72 | } 73 | 74 | exit(0); 75 | } 76 | 77 | ?> 78 | 79 | 80 | 81 | 82 | REF by digitalnature 83 | 111 | 112 | 113 |

REF

114 |

HTML output ~ TEXT output

115 |

created by digitalnature

116 | 117 | -------------------------------------------------------------------------------- /tests/today: -------------------------------------------------------------------------------- 1 | test 2 | test 3 | test 4 | test 5 | test 6 | test 7 | test 8 | test 9 | test 10 | test 11 | test 12 | test 13 | test 14 | test 15 | test 16 | test 17 | test 18 | test 19 | --------------------------------------------------------------------------------