├── .codeclimate.yml ├── .phpcs └── Barracuda │ ├── Sniffs │ ├── Commenting │ │ ├── DisallowSingleLineMultiCommentsSniff.php │ │ ├── DocCommentSniff.php │ │ ├── FunctionCommentSniff.php │ │ ├── SpaceAfterCommentSniff.php │ │ └── VariableCommentSniff.php │ ├── ControlStructures │ │ ├── ControlSignatureSniff.php │ │ └── NoInlineAssignmentSniff.php │ ├── Formatting │ │ └── SpaceUnaryOperatorSniff.php │ └── Functions │ │ └── FunctionDeclarationSniff.php │ ├── bootstrap.php │ └── ruleset.xml ├── LICENSE ├── README.md ├── composer.json ├── examples ├── blocking.php ├── bucket.php ├── identifier.php ├── nonblocking.php ├── result_callback.php └── result_deferred.php └── fork_daemon.php /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - ".phpcs/**/*" 3 | - "examples/**/*" 4 | engines: 5 | phpmd: 6 | enabled: false 7 | config: 8 | file_extensions: "php" 9 | rulesets: "unusedcode" 10 | phpcodesniffer: 11 | enabled: true 12 | config: 13 | file_extensions: "php" 14 | standard: "/code/.phpcs/Barracuda/ruleset.xml" 15 | ignore_warnings: true 16 | fixme: 17 | enabled: true 18 | ratings: 19 | paths: 20 | - "*.php" 21 | - "**/*.php" 22 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/Commenting/DisallowSingleLineMultiCommentsSniff.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence 11 | * @version 1.0.00 12 | * @link http://pear.php.net/package/PHP_CodeSniffer 13 | */ 14 | 15 | /** 16 | * This sniff prohibits the use of Perl style hash comments. 17 | * 18 | * An example of a hash comment is: 19 | * 20 | * 21 | * /* This is a single line multi line comment, which is prohibited. */ 22 | /* $hello = 'hello'; 23 | * 24 | * 25 | * @category PHP 26 | * @package PHP_CodeSniffer 27 | * @author Ryan Matthews 28 | * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence 29 | * @version Release: 1.0.00 30 | * @link http://pear.php.net/package/PHP_CodeSniffer 31 | */ 32 | class Barracuda_Sniffs_Commenting_DisallowSingleLineMultiCommentsSniff implements PHP_CodeSniffer_Sniff 33 | { 34 | 35 | 36 | /** 37 | * Returns the token types that this sniff is interested in. 38 | * 39 | * @return array(int) 40 | */ 41 | public function register() 42 | { 43 | return array(T_COMMENT); 44 | 45 | }// end register() 46 | 47 | 48 | /** 49 | * Processes the tokens that this sniff is interested in. 50 | * 51 | * @param PHP_CodeSniffer_File $phpcsFile The file where the token was found. 52 | * @param int $stackPtr The position in the stack where 53 | * the token was found. 54 | * 55 | * @return void 56 | */ 57 | public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 58 | { 59 | $tokens = $phpcsFile->getTokens(); 60 | if (preg_match('/\/\*[^\n]*\*\//', $tokens[$stackPtr]['content'])) 61 | { 62 | $error = 'Multi line comments are prohibited on single lines; found %s'; 63 | $data = array(trim($tokens[$stackPtr]['content'])); 64 | $phpcsFile->addError($error, $stackPtr, 'Found', $data); 65 | } 66 | 67 | }// end process() 68 | } 69 | // end class 70 | 71 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/Commenting/DocCommentSniff.php: -------------------------------------------------------------------------------- 1 | array( 13 | 'required' => false, 14 | 'allow_multiple' => false, 15 | ), 16 | '@package' => array( 17 | 'required' => false, 18 | 'allow_multiple' => false, 19 | ), 20 | '@subpackage' => array( 21 | 'required' => false, 22 | 'allow_multiple' => false, 23 | ), 24 | '@author' => array( 25 | 'required' => false, 26 | 'allow_multiple' => true, 27 | ), 28 | '@copyright' => array( 29 | 'required' => false, 30 | 'allow_multiple' => true, 31 | ), 32 | '@license' => array( 33 | 'required' => false, 34 | 'allow_multiple' => false, 35 | ), 36 | '@version' => array( 37 | 'required' => false, 38 | 'allow_multiple' => false, 39 | ), 40 | '@link' => array( 41 | 'required' => false, 42 | 'allow_multiple' => true, 43 | ), 44 | '@see' => array( 45 | 'required' => false, 46 | 'allow_multiple' => true, 47 | ), 48 | '@since' => array( 49 | 'required' => false, 50 | 'allow_multiple' => false, 51 | ), 52 | '@deprecated' => array( 53 | 'required' => false, 54 | 'allow_multiple' => false, 55 | ), 56 | ); 57 | 58 | 59 | /** 60 | * Returns an array of tokens this test wants to listen for. 61 | * 62 | * @return array 63 | */ 64 | public function register() 65 | { 66 | return array(T_OPEN_TAG); 67 | 68 | }//end register() 69 | 70 | 71 | /** 72 | * Processes this test, when one of its tokens is encountered. 73 | * 74 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 75 | * @param int $stackPtr The position of the current token 76 | * in the stack passed in $tokens. 77 | * 78 | * @return int 79 | */ 80 | public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 81 | { 82 | $tokens = $phpcsFile->getTokens(); 83 | 84 | // Find the next non whitespace token. 85 | $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); 86 | 87 | // Allow declare() statements at the top of the file. 88 | if ($tokens[$commentStart]['code'] === T_DECLARE) { 89 | $semicolon = $phpcsFile->findNext(T_SEMICOLON, ($commentStart + 1)); 90 | $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($semicolon + 1), null, true); 91 | } 92 | 93 | // Ignore vim header. 94 | if ($tokens[$commentStart]['code'] === T_COMMENT) { 95 | if (strstr($tokens[$commentStart]['content'], 'vim:') !== false) { 96 | $commentStart = $phpcsFile->findNext( 97 | T_WHITESPACE, 98 | ($commentStart + 1), 99 | null, 100 | true 101 | ); 102 | } 103 | } 104 | 105 | $errorToken = ($stackPtr + 1); 106 | if (isset($tokens[$errorToken]) === false) { 107 | $errorToken--; 108 | } 109 | 110 | if ($tokens[$commentStart]['code'] === T_CLOSE_TAG) { 111 | // We are only interested if this is the first open tag. 112 | return ($phpcsFile->numTokens + 1); 113 | } else if ($tokens[$commentStart]['code'] === T_COMMENT) { 114 | $error = 'You must use "/**" style comments for a file comment'; 115 | $phpcsFile->addError($error, $errorToken, 'WrongStyle'); 116 | $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'yes'); 117 | return ($phpcsFile->numTokens + 1); 118 | } else if ($commentStart === false 119 | || $tokens[$commentStart]['code'] !== T_DOC_COMMENT_OPEN_TAG 120 | ) { 121 | $phpcsFile->addError('Missing file doc comment', $errorToken, 'Missing'); 122 | $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'no'); 123 | return ($phpcsFile->numTokens + 1); 124 | } else { 125 | $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'yes'); 126 | } 127 | 128 | // Check the PHP Version, which should be in some text before the first tag. 129 | $commentEnd = $tokens[$commentStart]['comment_closer']; 130 | $found = false; 131 | for ($i = ($commentStart + 1); $i < $commentEnd; $i++) { 132 | if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) { 133 | break; 134 | } else if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING 135 | && strstr(strtolower($tokens[$i]['content']), 'php version') !== false 136 | ) { 137 | $found = true; 138 | break; 139 | } 140 | } 141 | 142 | if ($found === false) { 143 | $error = 'PHP version not specified'; 144 | $phpcsFile->addWarning($error, $commentEnd, 'MissingVersion'); 145 | } 146 | 147 | // Check each tag. 148 | $this->processTags($phpcsFile, $stackPtr, $commentStart); 149 | 150 | // Ignore the rest of the file. 151 | return ($phpcsFile->numTokens + 1); 152 | 153 | }//end process() 154 | 155 | 156 | /** 157 | * Processes each required or optional tag. 158 | * 159 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 160 | * @param int $stackPtr The position of the current token 161 | * in the stack passed in $tokens. 162 | * @param int $commentStart Position in the stack where the comment started. 163 | * 164 | * @return void 165 | */ 166 | protected function processTags(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart) 167 | { 168 | $tokens = $phpcsFile->getTokens(); 169 | 170 | if (get_class($this) === 'PEAR_Sniffs_Commenting_FileCommentSniff') { 171 | $docBlock = 'file'; 172 | } else { 173 | $docBlock = 'class'; 174 | } 175 | 176 | $commentEnd = $tokens[$commentStart]['comment_closer']; 177 | 178 | $foundTags = array(); 179 | $tagTokens = array(); 180 | foreach ($tokens[$commentStart]['comment_tags'] as $tag) { 181 | $name = $tokens[$tag]['content']; 182 | if (isset($this->tags[$name]) === false) { 183 | continue; 184 | } 185 | 186 | if ($this->tags[$name]['allow_multiple'] === false && isset($tagTokens[$name]) === true) { 187 | $error = 'Only one %s tag is allowed in a %s comment'; 188 | $data = array( 189 | $name, 190 | $docBlock, 191 | ); 192 | $phpcsFile->addError($error, $tag, 'Duplicate'.ucfirst(substr($name, 1)).'Tag', $data); 193 | } 194 | 195 | $foundTags[] = $name; 196 | $tagTokens[$name][] = $tag; 197 | 198 | $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd); 199 | if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) { 200 | $error = 'Content missing for %s tag in %s comment'; 201 | $data = array( 202 | $name, 203 | $docBlock, 204 | ); 205 | $phpcsFile->addError($error, $tag, 'Empty'.ucfirst(substr($name, 1)).'Tag', $data); 206 | continue; 207 | } 208 | }//end foreach 209 | 210 | // Check if the tags are in the correct position. 211 | $pos = 0; 212 | foreach ($this->tags as $tag => $tagData) { 213 | if (isset($tagTokens[$tag]) === false) { 214 | if ($tagData['required'] === true) { 215 | $error = 'Missing %s tag in %s comment'; 216 | $data = array( 217 | $tag, 218 | $docBlock, 219 | ); 220 | $phpcsFile->addError($error, $commentEnd, 'Missing'.ucfirst(substr($tag, 1)).'Tag', $data); 221 | } 222 | 223 | continue; 224 | } else { 225 | $method = 'process'.substr($tag, 1); 226 | if (method_exists($this, $method) === true) { 227 | // Process each tag if a method is defined. 228 | call_user_func(array($this, $method), $phpcsFile, $tagTokens[$tag]); 229 | } 230 | } 231 | 232 | if (isset($foundTags[$pos]) === false) { 233 | break; 234 | } 235 | 236 | if ($foundTags[$pos] !== $tag) { 237 | $error = 'The tag in position %s should be the %s tag'; 238 | $data = array( 239 | ($pos + 1), 240 | $tag, 241 | ); 242 | $phpcsFile->addError($error, $tokens[$commentStart]['comment_tags'][$pos], ucfirst(substr($tag, 1)).'TagOrder', $data); 243 | } 244 | 245 | // Account for multiple tags. 246 | $pos++; 247 | while (isset($foundTags[$pos]) === true && $foundTags[$pos] === $tag) { 248 | $pos++; 249 | } 250 | }//end foreach 251 | 252 | }//end processTags() 253 | 254 | 255 | /** 256 | * Process the category tag. 257 | * 258 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 259 | * @param array $tags The tokens for these tags. 260 | * 261 | * @return void 262 | */ 263 | protected function processCategory(PHP_CodeSniffer_File $phpcsFile, array $tags) 264 | { 265 | $tokens = $phpcsFile->getTokens(); 266 | foreach ($tags as $tag) { 267 | if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) { 268 | // No content. 269 | continue; 270 | } 271 | 272 | $content = $tokens[($tag + 2)]['content']; 273 | if (PHP_CodeSniffer::isUnderscoreName($content) !== true) { 274 | $newContent = str_replace(' ', '_', $content); 275 | $nameBits = explode('_', $newContent); 276 | $firstBit = array_shift($nameBits); 277 | $newName = ucfirst($firstBit).'_'; 278 | foreach ($nameBits as $bit) { 279 | if ($bit !== '') { 280 | $newName .= ucfirst($bit).'_'; 281 | } 282 | } 283 | 284 | $error = 'Category name "%s" is not valid; consider "%s" instead'; 285 | $validName = trim($newName, '_'); 286 | $data = array( 287 | $content, 288 | $validName, 289 | ); 290 | $phpcsFile->addError($error, $tag, 'InvalidCategory', $data); 291 | } 292 | }//end foreach 293 | 294 | }//end processCategory() 295 | 296 | 297 | /** 298 | * Process the package tag. 299 | * 300 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 301 | * @param array $tags The tokens for these tags. 302 | * 303 | * @return void 304 | */ 305 | protected function processPackage(PHP_CodeSniffer_File $phpcsFile, array $tags) 306 | { 307 | $tokens = $phpcsFile->getTokens(); 308 | foreach ($tags as $tag) { 309 | if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) { 310 | // No content. 311 | continue; 312 | } 313 | 314 | $content = $tokens[($tag + 2)]['content']; 315 | if (PHP_CodeSniffer::isUnderscoreName($content) === true) { 316 | continue; 317 | } 318 | 319 | $newContent = str_replace(' ', '_', $content); 320 | $newContent = trim($newContent, '_'); 321 | $newContent = preg_replace('/[^A-Za-z_]/', '', $newContent); 322 | $nameBits = explode('_', $newContent); 323 | $firstBit = array_shift($nameBits); 324 | $newName = strtoupper($firstBit{0}).substr($firstBit, 1).'_'; 325 | foreach ($nameBits as $bit) { 326 | if ($bit !== '') { 327 | $newName .= strtoupper($bit{0}).substr($bit, 1).'_'; 328 | } 329 | } 330 | 331 | $error = 'Package name "%s" is not valid; consider "%s" instead'; 332 | $validName = trim($newName, '_'); 333 | $data = array( 334 | $content, 335 | $validName, 336 | ); 337 | $phpcsFile->addError($error, $tag, 'InvalidPackage', $data); 338 | }//end foreach 339 | 340 | }//end processPackage() 341 | 342 | 343 | /** 344 | * Process the subpackage tag. 345 | * 346 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 347 | * @param array $tags The tokens for these tags. 348 | * 349 | * @return void 350 | */ 351 | protected function processSubpackage(PHP_CodeSniffer_File $phpcsFile, array $tags) 352 | { 353 | $tokens = $phpcsFile->getTokens(); 354 | foreach ($tags as $tag) { 355 | if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) { 356 | // No content. 357 | continue; 358 | } 359 | 360 | $content = $tokens[($tag + 2)]['content']; 361 | if (PHP_CodeSniffer::isUnderscoreName($content) === true) { 362 | continue; 363 | } 364 | 365 | $newContent = str_replace(' ', '_', $content); 366 | $nameBits = explode('_', $newContent); 367 | $firstBit = array_shift($nameBits); 368 | $newName = strtoupper($firstBit{0}).substr($firstBit, 1).'_'; 369 | foreach ($nameBits as $bit) { 370 | if ($bit !== '') { 371 | $newName .= strtoupper($bit{0}).substr($bit, 1).'_'; 372 | } 373 | } 374 | 375 | $error = 'Subpackage name "%s" is not valid; consider "%s" instead'; 376 | $validName = trim($newName, '_'); 377 | $data = array( 378 | $content, 379 | $validName, 380 | ); 381 | $phpcsFile->addError($error, $tag, 'InvalidSubpackage', $data); 382 | }//end foreach 383 | 384 | }//end processSubpackage() 385 | 386 | /** 387 | * Author tag must be 'Firstname Lastname '. 388 | * 389 | * @param int $errorPos The line number where the error occurs. 390 | * 391 | * @return void 392 | */ 393 | protected function processAuthors($errorPos) 394 | { 395 | $authors = $this->commentParser->getAuthors(); 396 | if (empty($authors) === false) { 397 | $author = $authors[0]; 398 | $content = $author->getContent(); 399 | if (empty($content) === true) { 400 | $error = 'Content missing for @author tag in file comment'; 401 | $this->currentFile->addError($error, $errorPos, 'MissingAuthor'); 402 | } else if (preg_match('/^(.*) \<.*\@.*\>$/', $content) === 0) { 403 | $error = 'Expected "Firstname Lastname " for author tag'; 404 | $this->currentFile->addError($error, $errorPos, 'IncorrectAuthor'); 405 | } 406 | } 407 | }//end processAuthors() 408 | 409 | /** 410 | * Copyright tag must be in the form 'xxxx-xxxx Barracuda Networks, Inc.'. 411 | * 412 | * @param int $errorPos The line number where the error occurs. 413 | * 414 | * @return void 415 | */ 416 | protected function processCopyrights($errorPos) 417 | { 418 | $copyrights = $this->commentParser->getCopyrights(); 419 | $copyright = $copyrights[0]; 420 | if ($copyright !== null) { 421 | $content = $copyright->getContent(); 422 | if (empty($content) === true) { 423 | $error = 'Content missing for @copyright tag in file comment'; 424 | $this->currentFile->addError($error, $errorPos, 'MissingCopyright'); 425 | } else if (preg_match('/^([0-9]{4})(-[0-9]{4})? (\.*\)$/', $content) === 0) { 426 | $error = 'Expected "xxxx-xxxx Barracuda Networks, Inc." for copyright declaration'; 427 | $this->currentFile->addError($error, $errorPos, 'IncorrectCopyright'); 428 | } 429 | } 430 | }//end processCopyrights() 431 | 432 | /** 433 | * Process the license tag. 434 | * 435 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 436 | * @param array $tags The tokens for these tags. 437 | * 438 | * @return void 439 | */ 440 | protected function processLicense(PHP_CodeSniffer_File $phpcsFile, array $tags) 441 | { 442 | $tokens = $phpcsFile->getTokens(); 443 | foreach ($tags as $tag) { 444 | if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) { 445 | // No content. 446 | continue; 447 | } 448 | 449 | $content = $tokens[($tag + 2)]['content']; 450 | $matches = array(); 451 | preg_match('/^([^\s]+)\s+(.*)/', $content, $matches); 452 | if (count($matches) !== 3) { 453 | $error = '@license tag must contain a URL and a license name'; 454 | $phpcsFile->addError($error, $tag, 'IncompleteLicense'); 455 | } 456 | } 457 | 458 | }//end processLicense() 459 | 460 | 461 | /** 462 | * Process the version tag. 463 | * 464 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 465 | * @param array $tags The tokens for these tags. 466 | * 467 | * @return void 468 | */ 469 | protected function processVersion(PHP_CodeSniffer_File $phpcsFile, array $tags) 470 | { 471 | $tokens = $phpcsFile->getTokens(); 472 | foreach ($tags as $tag) { 473 | if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) { 474 | // No content. 475 | continue; 476 | } 477 | 478 | $content = $tokens[($tag + 2)]['content']; 479 | if (strstr($content, 'CVS:') === false 480 | && strstr($content, 'SVN:') === false 481 | && strstr($content, 'GIT:') === false 482 | && strstr($content, 'HG:') === false 483 | ) { 484 | $error = 'Invalid version "%s" in file comment; consider "CVS: " or "SVN: " or "GIT: " or "HG: " instead'; 485 | $data = array($content); 486 | $phpcsFile->addWarning($error, $tag, 'InvalidVersion', $data); 487 | } 488 | } 489 | 490 | }//end processVersion() 491 | 492 | 493 | }//end class 494 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/Commenting/FunctionCommentSniff.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Marc McIntyre 11 | * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 12 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 13 | * @link http://pear.php.net/package/PHP_CodeSniffer 14 | */ 15 | 16 | if (class_exists('PEAR_Sniffs_Commenting_FunctionCommentSniff', true) === false) { 17 | throw new PHP_CodeSniffer_Exception('Class PEAR_Sniffs_Commenting_FunctionCommentSniff not found'); 18 | } 19 | 20 | /** 21 | * Parses and verifies the doc comments for functions. 22 | * 23 | * @category PHP 24 | * @package PHP_CodeSniffer 25 | * @author Greg Sherwood 26 | * @author Marc McIntyre 27 | * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 28 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 29 | * @version Release: @package_version@ 30 | * @link http://pear.php.net/package/PHP_CodeSniffer 31 | */ 32 | class Barracuda_Sniffs_Commenting_FunctionCommentSniff extends PEAR_Sniffs_Commenting_FunctionCommentSniff 33 | { 34 | protected $allowedTypes = array( 35 | 'int', 36 | 'bool', 37 | ); 38 | 39 | public function __construct() 40 | { 41 | PHP_CodeSniffer::$allowedTypes = array_merge(PHP_CodeSniffer::$allowedTypes, $this->allowedTypes); 42 | } 43 | 44 | /** 45 | * Process the return comment of this function comment. 46 | * 47 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 48 | * @param int $stackPtr The position of the current token 49 | * in the stack passed in $tokens. 50 | * @param int $commentStart The position in the stack where the comment started. 51 | * 52 | * @return void 53 | */ 54 | protected function processReturn(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart) 55 | { 56 | $tokens = $phpcsFile->getTokens(); 57 | 58 | // Skip constructor and destructor. 59 | $methodName = $phpcsFile->getDeclarationName($stackPtr); 60 | $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct'); 61 | 62 | $return = null; 63 | foreach ($tokens[$commentStart]['comment_tags'] as $tag) { 64 | if ($tokens[$tag]['content'] === '@return') { 65 | if ($return !== null) { 66 | $error = 'Only 1 @return tag is allowed in a function comment'; 67 | $phpcsFile->addError($error, $tag, 'DuplicateReturn'); 68 | return; 69 | } 70 | 71 | $return = $tag; 72 | } 73 | } 74 | 75 | if ($isSpecialMethod === true) { 76 | return; 77 | } 78 | 79 | if ($return !== null) { 80 | $content = $tokens[($return + 2)]['content']; 81 | if (empty($content) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) { 82 | $error = 'Return type missing for @return tag in function comment'; 83 | $phpcsFile->addError($error, $return, 'MissingReturnType'); 84 | } else { 85 | // Check return type (can be multiple, separated by '|'). 86 | $typeNames = explode('|', $content); 87 | $suggestedNames = array(); 88 | foreach ($typeNames as $i => $typeName) { 89 | $suggestedName = PHP_CodeSniffer::suggestType($typeName); 90 | if (in_array($suggestedName, $suggestedNames) === false) { 91 | $suggestedNames[] = $suggestedName; 92 | } 93 | } 94 | 95 | $suggestedType = implode('|', $suggestedNames); 96 | if ($content !== $suggestedType) { 97 | $error = 'Expected "%s" but found "%s" for function return type'; 98 | $data = array( 99 | $suggestedType, 100 | $content, 101 | ); 102 | $fix = $phpcsFile->addFixableError($error, $return, 'InvalidReturn', $data); 103 | if ($fix === true) { 104 | $phpcsFile->fixer->replaceToken(($return + 2), $suggestedType); 105 | } 106 | } 107 | 108 | // If the return type is void, make sure there is 109 | // no return statement in the function. 110 | if ($content === 'void') { 111 | if (isset($tokens[$stackPtr]['scope_closer']) === true) { 112 | $endToken = $tokens[$stackPtr]['scope_closer']; 113 | for ($returnToken = $stackPtr; $returnToken < $endToken; $returnToken++) { 114 | if ($tokens[$returnToken]['code'] === T_CLOSURE) { 115 | $returnToken = $tokens[$returnToken]['scope_closer']; 116 | continue; 117 | } 118 | 119 | if ($tokens[$returnToken]['code'] === T_RETURN 120 | || $tokens[$returnToken]['code'] === T_YIELD 121 | ) { 122 | break; 123 | } 124 | } 125 | 126 | if ($returnToken !== $endToken) { 127 | // If the function is not returning anything, just 128 | // exiting, then there is no problem. 129 | $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true); 130 | if ($tokens[$semicolon]['code'] !== T_SEMICOLON) { 131 | $error = 'Function return type is void, but function contains return statement'; 132 | $phpcsFile->addError($error, $return, 'InvalidReturnVoid'); 133 | } 134 | } 135 | }//end if 136 | } else if ($content !== 'mixed') { 137 | // If return type is not void, there needs to be a return statement 138 | // somewhere in the function that returns something. 139 | if (isset($tokens[$stackPtr]['scope_closer']) === true) { 140 | $endToken = $tokens[$stackPtr]['scope_closer']; 141 | $returnToken = $phpcsFile->findNext(array(T_RETURN, T_YIELD), $stackPtr, $endToken); 142 | if ($returnToken === false) { 143 | $error = 'Function return type is not void, but function has no return statement'; 144 | $phpcsFile->addError($error, $return, 'InvalidNoReturn'); 145 | } else { 146 | $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true); 147 | if ($tokens[$semicolon]['code'] === T_SEMICOLON) { 148 | $error = 'Function return type is not void, but function is returning void here'; 149 | $phpcsFile->addError($error, $returnToken, 'InvalidReturnNotVoid'); 150 | } 151 | } 152 | } 153 | }//end if 154 | }//end if 155 | } else { 156 | $error = 'Missing @return tag in function comment'; 157 | $phpcsFile->addError($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn'); 158 | }//end if 159 | 160 | }//end processReturn() 161 | 162 | 163 | /** 164 | * Process any throw tags that this function comment has. 165 | * 166 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 167 | * @param int $stackPtr The position of the current token 168 | * in the stack passed in $tokens. 169 | * @param int $commentStart The position in the stack where the comment started. 170 | * 171 | * @return void 172 | */ 173 | protected function processThrows(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart) 174 | { 175 | $tokens = $phpcsFile->getTokens(); 176 | 177 | $throws = array(); 178 | foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { 179 | if ($tokens[$tag]['content'] !== '@throws') { 180 | continue; 181 | } 182 | 183 | $exception = null; 184 | $comment = null; 185 | if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { 186 | $matches = array(); 187 | preg_match('/([^\s]+)(?:\s+(.*))?/', $tokens[($tag + 2)]['content'], $matches); 188 | $exception = $matches[1]; 189 | if (isset($matches[2]) === true && trim($matches[2]) !== '') { 190 | $comment = $matches[2]; 191 | } 192 | } 193 | 194 | if ($exception === null) { 195 | $error = 'Exception type and comment missing for @throws tag in function comment'; 196 | $phpcsFile->addError($error, $tag, 'InvalidThrows'); 197 | } else if ($comment === null) { 198 | $error = 'Comment missing for @throws tag in function comment'; 199 | $phpcsFile->addError($error, $tag, 'EmptyThrows'); 200 | } else { 201 | // Any strings until the next tag belong to this comment. 202 | if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) { 203 | $end = $tokens[$commentStart]['comment_tags'][($pos + 1)]; 204 | } else { 205 | $end = $tokens[$commentStart]['comment_closer']; 206 | } 207 | 208 | for ($i = ($tag + 3); $i < $end; $i++) { 209 | if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { 210 | $comment .= ' '.$tokens[$i]['content']; 211 | } 212 | } 213 | 214 | // Starts with a capital letter and ends with a fullstop. 215 | $firstChar = $comment{0}; 216 | if (strtoupper($firstChar) !== $firstChar) { 217 | $error = '@throws tag comment must start with a capital letter'; 218 | $phpcsFile->addError($error, ($tag + 2), 'ThrowsNotCapital'); 219 | } 220 | 221 | $lastChar = substr($comment, -1); 222 | if ($lastChar !== '.') { 223 | $error = '@throws tag comment must end with a full stop'; 224 | $phpcsFile->addError($error, ($tag + 2), 'ThrowsNoFullStop'); 225 | } 226 | }//end if 227 | }//end foreach 228 | 229 | }//end processThrows() 230 | 231 | 232 | /** 233 | * Process the function parameter comments. 234 | * 235 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 236 | * @param int $stackPtr The position of the current token 237 | * in the stack passed in $tokens. 238 | * @param int $commentStart The position in the stack where the comment started. 239 | * 240 | * @return void 241 | */ 242 | protected function processParams(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart) 243 | { 244 | $tokens = $phpcsFile->getTokens(); 245 | 246 | $params = array(); 247 | $maxType = 0; 248 | $maxVar = 0; 249 | foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { 250 | if ($tokens[$tag]['content'] !== '@param') { 251 | continue; 252 | } 253 | 254 | $type = ''; 255 | $typeSpace = 0; 256 | $var = ''; 257 | $varSpace = 0; 258 | $comment = ''; 259 | $commentLines = array(); 260 | if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { 261 | $matches = array(); 262 | preg_match('/([^$&]+)(?:((?:\$|&)[^\s]+)(?:(\s+)(.*))?)?/', $tokens[($tag + 2)]['content'], $matches); 263 | 264 | $typeLen = strlen($matches[1]); 265 | $type = trim($matches[1]); 266 | $typeSpace = ($typeLen - strlen($type)); 267 | $typeLen = strlen($type); 268 | if ($typeLen > $maxType) { 269 | $maxType = $typeLen; 270 | } 271 | 272 | if (isset($matches[2]) === true) { 273 | $var = $matches[2]; 274 | $varLen = strlen($var); 275 | if ($varLen > $maxVar) { 276 | $maxVar = $varLen; 277 | } 278 | 279 | if (isset($matches[4]) === true) { 280 | $varSpace = strlen($matches[3]); 281 | $comment = $matches[4]; 282 | $commentLines[] = array( 283 | 'comment' => $comment, 284 | 'token' => ($tag + 2), 285 | 'indent' => $varSpace, 286 | ); 287 | 288 | // Any strings until the next tag belong to this comment. 289 | if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) { 290 | $end = $tokens[$commentStart]['comment_tags'][($pos + 1)]; 291 | } else { 292 | $end = $tokens[$commentStart]['comment_closer']; 293 | } 294 | 295 | for ($i = ($tag + 3); $i < $end; $i++) { 296 | if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { 297 | $indent = 0; 298 | if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) { 299 | $indent = strlen($tokens[($i - 1)]['content']); 300 | } 301 | 302 | $comment .= ' '.$tokens[$i]['content']; 303 | $commentLines[] = array( 304 | 'comment' => $tokens[$i]['content'], 305 | 'token' => $i, 306 | 'indent' => $indent, 307 | ); 308 | } 309 | } 310 | } else { 311 | $error = 'Missing parameter comment'; 312 | $phpcsFile->addError($error, $tag, 'MissingParamComment'); 313 | $commentLines[] = array('comment' => ''); 314 | }//end if 315 | } else { 316 | $error = 'Missing parameter name'; 317 | $phpcsFile->addError($error, $tag, 'MissingParamName'); 318 | }//end if 319 | } else { 320 | $error = 'Missing parameter type'; 321 | $phpcsFile->addError($error, $tag, 'MissingParamType'); 322 | }//end if 323 | 324 | $params[] = array( 325 | 'tag' => $tag, 326 | 'type' => $type, 327 | 'var' => $var, 328 | 'comment' => $comment, 329 | 'commentLines' => $commentLines, 330 | 'type_space' => $typeSpace, 331 | 'var_space' => $varSpace, 332 | ); 333 | }//end foreach 334 | 335 | $realParams = $phpcsFile->getMethodParameters($stackPtr); 336 | $foundParams = array(); 337 | 338 | foreach ($params as $pos => $param) { 339 | // If the type is empty, the whole line is empty. 340 | if ($param['type'] === '') { 341 | continue; 342 | } 343 | 344 | // Check the param type value. 345 | $typeNames = explode('|', $param['type']); 346 | foreach ($typeNames as $typeName) { 347 | $suggestedName = PHP_CodeSniffer::suggestType($typeName); 348 | if ($typeName !== $suggestedName) { 349 | $error = 'Expected "%s" but found "%s" for parameter type'; 350 | $data = array( 351 | $suggestedName, 352 | $typeName, 353 | ); 354 | 355 | $fix = $phpcsFile->addFixableError($error, $param['tag'], 'IncorrectParamVarName', $data); 356 | if ($fix === true) { 357 | $content = $suggestedName; 358 | $content .= str_repeat(' ', $param['type_space']); 359 | $content .= $param['var']; 360 | $content .= str_repeat(' ', $param['var_space']); 361 | if (isset($param['commentLines'][0]) === true) { 362 | $content .= $param['commentLines'][0]['comment']; 363 | } 364 | 365 | $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content); 366 | } 367 | } else if (count($typeNames) === 1) { 368 | // Check type hint for array and custom type. 369 | $suggestedTypeHint = ''; 370 | if (strpos($suggestedName, 'array') !== false || substr($suggestedName, -2) === '[]') { 371 | $suggestedTypeHint = 'array'; 372 | } else if (strpos($suggestedName, 'callable') !== false) { 373 | $suggestedTypeHint = 'callable'; 374 | } else if (strpos($suggestedName, 'callback') !== false) { 375 | $suggestedTypeHint = 'callable'; 376 | } else if (in_array($typeName, PHP_CodeSniffer::$allowedTypes) === false) { 377 | $suggestedTypeHint = $suggestedName; 378 | } 379 | 380 | if ($suggestedTypeHint !== '' && isset($realParams[$pos]) === true) { 381 | $typeHint = $realParams[$pos]['type_hint']; 382 | if ($typeHint === '') { 383 | $error = 'Type hint "%s" missing for %s'; 384 | $data = array( 385 | $suggestedTypeHint, 386 | $param['var'], 387 | ); 388 | $phpcsFile->addError($error, $stackPtr, 'TypeHintMissing', $data); 389 | } else if ($typeHint !== substr($suggestedTypeHint, (strlen($typeHint) * -1))) { 390 | $error = 'Expected type hint "%s"; found "%s" for %s'; 391 | $data = array( 392 | $suggestedTypeHint, 393 | $typeHint, 394 | $param['var'], 395 | ); 396 | $phpcsFile->addError($error, $stackPtr, 'IncorrectTypeHint', $data); 397 | } 398 | } else if ($suggestedTypeHint === '' && isset($realParams[$pos]) === true) { 399 | $typeHint = $realParams[$pos]['type_hint']; 400 | if ($typeHint !== '') { 401 | $error = 'Unknown type hint "%s" found for %s'; 402 | $data = array( 403 | $typeHint, 404 | $param['var'], 405 | ); 406 | $phpcsFile->addError($error, $stackPtr, 'InvalidTypeHint', $data); 407 | } 408 | }//end if 409 | }//end if 410 | }//end foreach 411 | 412 | if ($param['var'] === '') { 413 | continue; 414 | } 415 | 416 | $foundParams[] = $param['var']; 417 | 418 | // Check number of spaces after the type. 419 | $spaces = ($maxType - strlen($param['type']) + 1); 420 | if ($param['type_space'] !== $spaces) { 421 | $error = 'Expected %s spaces after parameter type; %s found'; 422 | $data = array( 423 | $spaces, 424 | $param['type_space'], 425 | ); 426 | 427 | $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamType', $data); 428 | if ($fix === true) { 429 | $phpcsFile->fixer->beginChangeset(); 430 | 431 | $content = $param['type']; 432 | $content .= str_repeat(' ', $spaces); 433 | $content .= $param['var']; 434 | $content .= str_repeat(' ', $param['var_space']); 435 | $content .= $param['commentLines'][0]['comment']; 436 | $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content); 437 | 438 | // Fix up the indent of additional comment lines. 439 | foreach ($param['commentLines'] as $lineNum => $line) { 440 | if ($lineNum === 0 441 | || $param['commentLines'][$lineNum]['indent'] === 0 442 | ) { 443 | continue; 444 | } 445 | 446 | $newIndent = ($param['commentLines'][$lineNum]['indent'] + $spaces - $param['type_space']); 447 | $phpcsFile->fixer->replaceToken( 448 | ($param['commentLines'][$lineNum]['token'] - 1), 449 | str_repeat(' ', $newIndent) 450 | ); 451 | } 452 | 453 | $phpcsFile->fixer->endChangeset(); 454 | }//end if 455 | }//end if 456 | 457 | // Make sure the param name is correct. 458 | if (isset($realParams[$pos]) === true) { 459 | $realName = $realParams[$pos]['name']; 460 | if ($realName !== $param['var']) { 461 | $code = 'ParamNameNoMatch'; 462 | $data = array( 463 | $param['var'], 464 | $realName, 465 | ); 466 | 467 | $error = 'Doc comment for parameter %s does not match '; 468 | if (strtolower($param['var']) === strtolower($realName)) { 469 | $error .= 'case of '; 470 | $code = 'ParamNameNoCaseMatch'; 471 | } 472 | 473 | $error .= 'actual variable name %s'; 474 | 475 | $phpcsFile->addError($error, $param['tag'], $code, $data); 476 | } 477 | } else if (substr($param['var'], -4) !== ',...') { 478 | // We must have an extra parameter comment. 479 | $error = 'Superfluous parameter comment'; 480 | $phpcsFile->addError($error, $param['tag'], 'ExtraParamComment'); 481 | }//end if 482 | 483 | if ($param['comment'] === '') { 484 | continue; 485 | } 486 | 487 | // Check number of spaces after the var name. 488 | $spaces = ($maxVar - strlen($param['var']) + 1); 489 | if ($param['var_space'] !== $spaces) { 490 | $error = 'Expected %s spaces after parameter name; %s found'; 491 | $data = array( 492 | $spaces, 493 | $param['var_space'], 494 | ); 495 | 496 | $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamName', $data); 497 | if ($fix === true) { 498 | $phpcsFile->fixer->beginChangeset(); 499 | 500 | $content = $param['type']; 501 | $content .= str_repeat(' ', $param['type_space']); 502 | $content .= $param['var']; 503 | $content .= str_repeat(' ', $spaces); 504 | $content .= $param['commentLines'][0]['comment']; 505 | $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content); 506 | 507 | // Fix up the indent of additional comment lines. 508 | foreach ($param['commentLines'] as $lineNum => $line) { 509 | if ($lineNum === 0 510 | || $param['commentLines'][$lineNum]['indent'] === 0 511 | ) { 512 | continue; 513 | } 514 | 515 | $newIndent = ($param['commentLines'][$lineNum]['indent'] + $spaces - $param['var_space']); 516 | $phpcsFile->fixer->replaceToken( 517 | ($param['commentLines'][$lineNum]['token'] - 1), 518 | str_repeat(' ', $newIndent) 519 | ); 520 | } 521 | 522 | $phpcsFile->fixer->endChangeset(); 523 | }//end if 524 | }//end if 525 | 526 | // Param comments must start with a capital letter and end with the full stop. 527 | $firstChar = $param['comment']{0}; 528 | if (preg_match('|\p{Lu}|u', $firstChar) === 0) { 529 | $error = 'Parameter comment must start with a capital letter'; 530 | $phpcsFile->addError($error, $param['tag'], 'ParamCommentNotCapital'); 531 | } 532 | 533 | $lastChar = substr($param['comment'], -1); 534 | if ($lastChar !== '.') { 535 | $error = 'Parameter comment must end with a full stop'; 536 | $phpcsFile->addError($error, $param['tag'], 'ParamCommentFullStop'); 537 | } 538 | }//end foreach 539 | 540 | $realNames = array(); 541 | foreach ($realParams as $realParam) { 542 | $realNames[] = $realParam['name']; 543 | } 544 | 545 | // Report missing comments. 546 | $diff = array_diff($realNames, $foundParams); 547 | foreach ($diff as $neededParam) { 548 | $error = 'Doc comment for parameter "%s" missing'; 549 | $data = array($neededParam); 550 | $phpcsFile->addError($error, $commentStart, 'MissingParamTag', $data); 551 | } 552 | 553 | }//end processParams() 554 | 555 | 556 | }//end class 557 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/Commenting/SpaceAfterCommentSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 28 | 29 | $valid = false; 30 | 31 | if (preg_match('|//\s|', $tokens[$stackPtr]['content'])) 32 | { 33 | $valid = true; 34 | } 35 | 36 | if (preg_match('|\*[\s/]|', $tokens[$stackPtr]['content'])) 37 | { 38 | $valid = true; 39 | } 40 | 41 | if ($valid === false) 42 | { 43 | $error = 'A space is required at the start of the comment %s'; 44 | $data = array(trim($tokens[$stackPtr]['content'])); 45 | $phpcsFile->addError($error, $stackPtr, 'Found', $data); 46 | } 47 | 48 | }// end process() 49 | } 50 | // end class 51 | 52 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/Commenting/VariableCommentSniff.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Marc McIntyre 11 | * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 12 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 13 | * @link http://pear.php.net/package/PHP_CodeSniffer 14 | */ 15 | 16 | if (class_exists('PHP_CodeSniffer_Standards_AbstractVariableSniff', true) === false) { 17 | throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_Standards_AbstractVariableSniff not found'); 18 | } 19 | 20 | /** 21 | * Parses and verifies the variable doc comment. 22 | * 23 | * @category PHP 24 | * @package PHP_CodeSniffer 25 | * @author Greg Sherwood 26 | * @author Marc McIntyre 27 | * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 28 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 29 | * @version Release: @package_version@ 30 | * @link http://pear.php.net/package/PHP_CodeSniffer 31 | */ 32 | 33 | class Barracuda_Sniffs_Commenting_VariableCommentSniff extends PHP_CodeSniffer_Standards_AbstractVariableSniff 34 | { 35 | protected $allowedTypes = array( 36 | 'bool', 37 | 'int', 38 | ); 39 | 40 | public function __construct() 41 | { 42 | PHP_CodeSniffer::$allowedTypes = array_merge(PHP_CodeSniffer::$allowedTypes, $this->allowedTypes); 43 | } 44 | 45 | /** 46 | * Called to process class member vars. 47 | * 48 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 49 | * @param int $stackPtr The position of the current token 50 | * in the stack passed in $tokens. 51 | * 52 | * @return void 53 | */ 54 | public function processMemberVar(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 55 | { 56 | $tokens = $phpcsFile->getTokens(); 57 | $commentToken = array( 58 | T_COMMENT, 59 | T_DOC_COMMENT_CLOSE_TAG, 60 | ); 61 | 62 | $commentEnd = $phpcsFile->findPrevious($commentToken, $stackPtr); 63 | if ($commentEnd === false) { 64 | $phpcsFile->addError('Missing member variable doc comment', $stackPtr, 'Missing'); 65 | return; 66 | } 67 | 68 | if ($tokens[$commentEnd]['code'] === T_COMMENT) { 69 | $phpcsFile->addError('You must use "/**" style comments for a member variable comment', $stackPtr, 'WrongStyle'); 70 | return; 71 | } else if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG) { 72 | $phpcsFile->addError('Missing member variable doc comment', $stackPtr, 'Missing'); 73 | return; 74 | } else { 75 | // Make sure the comment we have found belongs to us. 76 | $commentFor = $phpcsFile->findNext(array(T_VARIABLE, T_CLASS, T_INTERFACE), ($commentEnd + 1)); 77 | if ($commentFor !== $stackPtr) { 78 | $phpcsFile->addError('Missing member variable doc comment', $stackPtr, 'Missing'); 79 | return; 80 | } 81 | } 82 | 83 | $commentStart = $tokens[$commentEnd]['comment_opener']; 84 | 85 | $foundVar = null; 86 | foreach ($tokens[$commentStart]['comment_tags'] as $tag) { 87 | if ($tokens[$tag]['content'] === '@var') { 88 | if ($foundVar !== null) { 89 | $error = 'Only one @var tag is allowed in a member variable comment'; 90 | $phpcsFile->addError($error, $tag, 'DuplicateVar'); 91 | } else { 92 | $foundVar = $tag; 93 | } 94 | } else if ($tokens[$tag]['content'] === '@see') { 95 | // Make sure the tag isn't empty. 96 | $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd); 97 | if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) { 98 | $error = 'Content missing for @see tag in member variable comment'; 99 | $phpcsFile->addError($error, $tag, 'EmptySees'); 100 | } 101 | } else { 102 | $error = '%s tag is not allowed in member variable comment'; 103 | $data = array($tokens[$tag]['content']); 104 | $phpcsFile->addWarning($error, $tag, 'TagNotAllowed', $data); 105 | }//end if 106 | }//end foreach 107 | 108 | // The @var tag is the only one we require. 109 | if ($foundVar === null) { 110 | $error = 'Missing @var tag in member variable comment'; 111 | $phpcsFile->addError($error, $commentEnd, 'MissingVar'); 112 | return; 113 | } 114 | 115 | $firstTag = $tokens[$commentStart]['comment_tags'][0]; 116 | if ($foundVar !== null && $tokens[$firstTag]['content'] !== '@var') { 117 | $error = 'The @var tag must be the first tag in a member variable comment'; 118 | $phpcsFile->addError($error, $foundVar, 'VarOrder'); 119 | } 120 | 121 | // Make sure the tag isn't empty and has the correct padding. 122 | $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $foundVar, $commentEnd); 123 | if ($string === false || $tokens[$string]['line'] !== $tokens[$foundVar]['line']) { 124 | $error = 'Content missing for @var tag in member variable comment'; 125 | $phpcsFile->addError($error, $foundVar, 'EmptyVar'); 126 | return; 127 | } 128 | 129 | $varType = $tokens[($foundVar + 2)]['content']; 130 | $suggestedType = PHP_CodeSniffer::suggestType($varType); 131 | if ($varType !== $suggestedType) { 132 | $error = 'Expected "%s" but found "%s" for @var tag in member variable comment'; 133 | $data = array( 134 | $suggestedType, 135 | $varType, 136 | ); 137 | $phpcsFile->addError($error, ($foundVar + 2), 'IncorrectVarType', $data); 138 | } 139 | 140 | }//end processMemberVar() 141 | 142 | 143 | /** 144 | * Called to process a normal variable. 145 | * 146 | * Not required for this sniff. 147 | * 148 | * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where this token was found. 149 | * @param int $stackPtr The position where the double quoted 150 | * string was found. 151 | * 152 | * @return void 153 | */ 154 | protected function processVariable(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 155 | { 156 | 157 | }//end processVariable() 158 | 159 | 160 | /** 161 | * Called to process variables found in double quoted strings. 162 | * 163 | * Not required for this sniff. 164 | * 165 | * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where this token was found. 166 | * @param int $stackPtr The position where the double quoted 167 | * string was found. 168 | * 169 | * @return void 170 | */ 171 | protected function processVariableInString(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 172 | { 173 | 174 | }//end processVariableInString() 175 | 176 | 177 | }//end class 178 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/ControlStructures/ControlSignatureSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 30 | 31 | if (isset($tokens[($stackPtr + 1)]) === false) { 32 | return; 33 | } 34 | 35 | // Single space after the keyword. 36 | if (in_array($tokens[$stackPtr]['code'], array(T_CATCH, T_IF, T_WHILE, T_FOR, T_FOREACH, T_ELSEIF, T_SWITCH))) { 37 | $found = 1; 38 | if ($tokens[($stackPtr + 1)]['code'] !== T_WHITESPACE) { 39 | $found = 0; 40 | } else if ($tokens[($stackPtr + 1)]['content'] !== ' ') { 41 | if (strpos($tokens[($stackPtr + 1)]['content'], $phpcsFile->eolChar) !== false) { 42 | $found = 'newline'; 43 | } else { 44 | $found = strlen($tokens[($stackPtr + 1)]['content']); 45 | } 46 | } 47 | 48 | if ($found !== 1) { 49 | $error = 'Expected 1 space after %s keyword; %s found'; 50 | $data = array( 51 | strtoupper($tokens[$stackPtr]['content']), 52 | $found, 53 | ); 54 | 55 | $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterKeyword', $data); 56 | if ($fix === true) { 57 | if ($found === 0) { 58 | $phpcsFile->fixer->addContent($stackPtr, ' '); 59 | } else { 60 | $phpcsFile->fixer->replaceToken(($stackPtr + 1), ' '); 61 | } 62 | } 63 | } 64 | } 65 | 66 | // Single newline after the keyword. 67 | if (in_array($tokens[$stackPtr]['code'], array(T_TRY, T_DO, T_ELSE)) 68 | && isset($tokens[$stackPtr]['scope_opener']) === true 69 | ) { 70 | $opener = $tokens[$stackPtr]['scope_opener']; 71 | $found = ($tokens[$opener]['line'] - $tokens[$stackPtr]['line']); 72 | if ($found !== 1) { 73 | $error = 'Expected 1 newline after % keyword; %s found'; 74 | $data = array( 75 | strtoupper($tokens[$stackPtr]['content']), 76 | $found, 77 | ); 78 | $fix = $phpcsFile->addFixableError($error, $opener, 'NewlineAfterKeyword', $data); 79 | if ($fix === true) { 80 | $phpcsFile->fixer->beginChangeset(); 81 | for ($i = ($stackPtr + 1); $i < $opener; $i++) { 82 | if ($found > 0 && $tokens[$i]['line'] === $tokens[$opener]['line']) { 83 | break; 84 | } 85 | 86 | $phpcsFile->fixer->replaceToken($i, ''); 87 | } 88 | 89 | $phpcsFile->fixer->addContent($stackPtr, $phpcsFile->eolChar); 90 | $phpcsFile->fixer->endChangeset(); 91 | } 92 | }//end if 93 | }//end if 94 | 95 | // Single newline after closing parenthesis. 96 | if (isset($tokens[$stackPtr]['parenthesis_closer']) === true 97 | && isset($tokens[$stackPtr]['scope_opener']) === true 98 | ) { 99 | $closer = $tokens[$stackPtr]['parenthesis_closer']; 100 | $opener = $tokens[$stackPtr]['scope_opener']; 101 | $found = ($tokens[$opener]['line'] - $tokens[$closer]['line']); 102 | if ($found !== 1) { 103 | $error = 'Expected 1 newline after closing parenthesis; %s found'; 104 | $fix = $phpcsFile->addFixableError($error, $opener, 'NewlineAfterCloseParenthesis', array($found)); 105 | if ($fix === true) { 106 | $phpcsFile->fixer->beginChangeset(); 107 | for ($i = ($closer + 1); $i < $opener; $i++) { 108 | if ($found > 0 && $tokens[$i]['line'] === $tokens[$opener]['line']) { 109 | break; 110 | } 111 | 112 | $phpcsFile->fixer->replaceToken($i, ''); 113 | } 114 | 115 | $phpcsFile->fixer->addContent($closer, $phpcsFile->eolChar); 116 | $phpcsFile->fixer->endChangeset(); 117 | } 118 | }//end if 119 | }//end if 120 | 121 | // Single newline after opening brace. 122 | if (isset($tokens[$stackPtr]['scope_opener']) === true) { 123 | $opener = $tokens[$stackPtr]['scope_opener']; 124 | for ($next = ($opener + 1); $next < $phpcsFile->numTokens; $next++) { 125 | $code = $tokens[$next]['code']; 126 | 127 | if ($code === T_WHITESPACE) { 128 | continue; 129 | } 130 | 131 | // Skip all empty tokens on the same line as the opener. 132 | if ($tokens[$next]['line'] === $tokens[$opener]['line'] 133 | && (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$code]) === true 134 | || $code === T_CLOSE_TAG) 135 | ) { 136 | continue; 137 | } 138 | 139 | // We found the first bit of a code, or a comment on the 140 | // following line. 141 | break; 142 | } 143 | 144 | $found = ($tokens[$next]['line'] - $tokens[$opener]['line']); 145 | if ($found !== 1) { 146 | $error = 'Expected 1 newline after opening brace; %s found'; 147 | $data = array($found); 148 | $fix = $phpcsFile->addFixableError($error, $opener, 'NewlineAfterOpenBrace', $data); 149 | if ($fix === true) { 150 | $phpcsFile->fixer->beginChangeset(); 151 | for ($i = ($opener + 1); $i < $next; $i++) { 152 | if ($found > 0 && $tokens[$i]['line'] === $tokens[$next]['line']) { 153 | break; 154 | } 155 | 156 | $phpcsFile->fixer->replaceToken($i, ''); 157 | } 158 | 159 | $phpcsFile->fixer->addContent($opener, $phpcsFile->eolChar); 160 | $phpcsFile->fixer->endChangeset(); 161 | } 162 | } 163 | } else if ($tokens[$stackPtr]['code'] === T_WHILE) { 164 | // Zero spaces after parenthesis closer. 165 | $closer = $tokens[$stackPtr]['parenthesis_closer']; 166 | $found = 0; 167 | if ($tokens[($closer + 1)]['code'] === T_WHITESPACE) { 168 | if (strpos($tokens[($closer + 1)]['content'], $phpcsFile->eolChar) !== false) { 169 | $found = 'newline'; 170 | } else { 171 | $found = strlen($tokens[($closer + 1)]['content']); 172 | } 173 | } 174 | 175 | if ($found !== 0) { 176 | $error = 'Expected 0 spaces before semicolon; %s found'; 177 | $data = array($found); 178 | $fix = $phpcsFile->addFixableError($error, $closer, 'SpaceBeforeSemicolon', $data); 179 | if ($fix === true) { 180 | $phpcsFile->fixer->replaceToken(($closer + 1), ''); 181 | } 182 | } 183 | }//end if 184 | 185 | // Only want to check multi-keyword structures from here on. 186 | if ($tokens[$stackPtr]['code'] === T_DO) { 187 | if (isset($tokens[$stackPtr]['scope_closer']) === false) { 188 | return; 189 | } 190 | 191 | $closer = $tokens[$stackPtr]['scope_closer']; 192 | } else if ($tokens[$stackPtr]['code'] === T_ELSE 193 | || $tokens[$stackPtr]['code'] === T_ELSEIF 194 | || $tokens[$stackPtr]['code'] === T_CATCH 195 | ) { 196 | $closer = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr - 1), null, true); 197 | if ($closer === false || $tokens[$closer]['code'] !== T_CLOSE_CURLY_BRACKET) { 198 | return; 199 | } 200 | } else { 201 | return; 202 | } 203 | 204 | // Single space after closing brace. 205 | if ($tokens[$stackPtr]['code'] === T_DO) { 206 | $found = 1; 207 | if ($tokens[($closer + 1)]['code'] !== T_WHITESPACE) { 208 | $found = 0; 209 | } else if ($tokens[($closer + 1)]['content'] !== ' ') { 210 | if (strpos($tokens[($closer + 1)]['content'], $phpcsFile->eolChar) !== false) { 211 | $found = 'newline'; 212 | } else { 213 | $found = strlen($tokens[($closer + 1)]['content']); 214 | } 215 | } 216 | 217 | if ($found !== 1) { 218 | $error = 'Expected 1 space after closing brace; %s found'; 219 | $data = array($found); 220 | $fix = $phpcsFile->addFixableError($error, $closer, 'SpaceAfterCloseBrace', $data); 221 | if ($fix === true) { 222 | if ($found === 0) { 223 | $phpcsFile->fixer->addContent($closer, ' '); 224 | } else { 225 | $phpcsFile->fixer->replaceToken(($closer + 1), ' '); 226 | } 227 | } 228 | } 229 | } 230 | 231 | // Single newline after closing brace. 232 | if ($tokens[$stackPtr]['code'] !== T_DO) { 233 | for ($next = ($closer + 1); $next < $phpcsFile->numTokens; $next++) { 234 | $code = $tokens[$next]['code']; 235 | 236 | if ($code === T_WHITESPACE) { 237 | continue; 238 | } 239 | 240 | // Skip all empty tokens on the same line as the closer. 241 | if ($tokens[$next]['line'] === $tokens[$closer]['line'] 242 | && (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$code]) === true 243 | || $code === T_CLOSE_TAG) 244 | ) { 245 | continue; 246 | } 247 | 248 | // We found the first bit of a code, or a comment on the 249 | // following line. 250 | break; 251 | } 252 | 253 | $found = ($tokens[$next]['line'] - $tokens[$closer]['line']); 254 | if ($found !== 1) { 255 | $error = 'Expected 1 newline after closing brace; %s found'; 256 | $data = array($found); 257 | $fix = $phpcsFile->addFixableError($error, $closer, 'NewlineAfterCloseBrace', $data); 258 | if ($fix === true) { 259 | $phpcsFile->fixer->beginChangeset(); 260 | for ($i = ($closer + 1); $i < $next; $i++) { 261 | if ($found > 0 && $tokens[$i]['line'] === $tokens[$next]['line']) { 262 | break; 263 | } 264 | 265 | $phpcsFile->fixer->replaceToken($i, ''); 266 | } 267 | 268 | $phpcsFile->fixer->addContent($closer, $phpcsFile->eolChar); 269 | $phpcsFile->fixer->endChangeset(); 270 | } 271 | } 272 | } 273 | 274 | }//end process() 275 | } 276 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/ControlStructures/NoInlineAssignmentSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 32 | 33 | $end_position = $tokens[$stackPtr]['parenthesis_closer']; 34 | 35 | // states: -1 = normal, 0 = start function call (probably), 1 = in function 36 | $function = -1; 37 | 38 | for ($position = $stackPtr; $position < $end_position; $position++) 39 | { 40 | if ($tokens[$position]['type'] == 'T_STRING') 41 | { 42 | $function = 0; 43 | continue; 44 | } 45 | 46 | if ($function === 0) 47 | { 48 | if ($tokens[$position]['type'] == 'T_OPEN_PARENTHESIS') 49 | { 50 | $function = 1; 51 | continue; 52 | } 53 | } 54 | elseif ($function !== 1) 55 | { 56 | $function = -1; 57 | if ($tokens[$position]['type'] == 'T_EQUAL') 58 | { 59 | $error = 'Inline assignment not allowed in if statements'; 60 | $phpcsFile->addError($error, $stackPtr, 'IncDecLeft'); 61 | return; 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/Formatting/SpaceUnaryOperatorSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 28 | 29 | // Check decrement / increment. 30 | if ($tokens[$stackPtr]['code'] === T_DEC || $tokens[$stackPtr]['code'] === T_INC) 31 | { 32 | $modifyLeft = substr($tokens[($stackPtr - 1)]['content'], 0, 1) === '$' || 33 | $tokens[($stackPtr + 1)]['content'] === ';'; 34 | 35 | if ($modifyLeft === true && $tokens[($stackPtr - 1)]['code'] === T_WHITESPACE) 36 | { 37 | $error = 'There must not be a single space before a unary operator statement'; 38 | $phpcsFile->addError($error, $stackPtr, 'IncDecLeft'); 39 | return; 40 | } 41 | 42 | if ($modifyLeft === false && !in_array(substr($tokens[($stackPtr + 1)]['content'], 0, 1), array('$', ','))) 43 | { 44 | $error = 'A unary operator statement must not be followed by a single space'; 45 | $phpcsFile->addError($error, $stackPtr, 'IncDecRight'); 46 | return; 47 | } 48 | } 49 | 50 | // Check "!" operator. 51 | if ($tokens[$stackPtr]['code'] === T_BOOLEAN_NOT && $tokens[$stackPtr + 1]['code'] === T_WHITESPACE) 52 | { 53 | $error = 'A unary operator statement must not be followed by a space'; 54 | $phpcsFile->addError($error, $stackPtr, 'BooleanNot'); 55 | return; 56 | } 57 | 58 | // Find the last syntax item to determine if this is an unary operator. 59 | $lastSyntaxItem = $phpcsFile->findPrevious( 60 | array(T_WHITESPACE), 61 | $stackPtr - 1, 62 | ($tokens[$stackPtr]['column']) * -1, 63 | true, 64 | null, 65 | true 66 | ); 67 | $operatorSuffixAllowed = in_array( 68 | $tokens[$lastSyntaxItem]['code'], 69 | array( 70 | T_LNUMBER, 71 | T_DNUMBER, 72 | T_CLOSE_PARENTHESIS, 73 | T_CLOSE_CURLY_BRACKET, 74 | T_CLOSE_SQUARE_BRACKET, 75 | T_VARIABLE, 76 | T_STRING, 77 | ) 78 | ); 79 | 80 | // Check plus / minus value assignments or comparisons. 81 | if ($tokens[$stackPtr]['code'] === T_MINUS || $tokens[$stackPtr]['code'] === T_PLUS) 82 | { 83 | if ($operatorSuffixAllowed === false 84 | && $tokens[($stackPtr + 1)]['code'] === T_WHITESPACE 85 | ) 86 | { 87 | $error = 'A unary operator statement must not be followed by a space'; 88 | $phpcsFile->addError($error, $stackPtr); 89 | } 90 | } 91 | 92 | } // end process() 93 | // end class 94 | } 95 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/Functions/FunctionDeclarationSniff.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600) 11 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 12 | * @link http://pear.php.net/package/PHP_CodeSniffer 13 | */ 14 | 15 | if (class_exists('PEAR_Sniffs_Functions_FunctionDeclarationSniff', true) === false) { 16 | $error = 'Class PEAR_Sniffs_Functions_FunctionDeclarationSniff not found'; 17 | throw new PHP_CodeSniffer_Exception($error); 18 | } 19 | 20 | /** 21 | * Barracuda_Sniffs_Functions_MultiLineFunctionDeclarationSniff. 22 | * 23 | * Ensure single and multi-line function declarations are defined correctly. 24 | * 25 | * @category PHP 26 | * @package PHP_CodeSniffer 27 | * @author Greg Sherwood 28 | * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600) 29 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 30 | * @version Release: 1.5.0RC4 31 | * @link http://pear.php.net/package/PHP_CodeSniffer 32 | */ 33 | class Barracuda_Sniffs_Functions_FunctionDeclarationSniff extends PEAR_Sniffs_Functions_FunctionDeclarationSniff 34 | { 35 | 36 | 37 | /** 38 | * Processes multi-line declarations. 39 | * 40 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 41 | * @param int $stackPtr The position of the current token 42 | * in the stack passed in $tokens. 43 | * @param array $tokens The stack of tokens that make up 44 | * the file. 45 | * 46 | * @return void 47 | */ 48 | public function processMultiLineDeclaration(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $tokens) 49 | { 50 | // We need to work out how far indented the function 51 | // declaration itself is, so we can work out how far to 52 | // indent parameters. 53 | $functionIndent = 0; 54 | for ($i = ($stackPtr - 1); $i >= 0; $i--) { 55 | if ($tokens[$i]['line'] !== $tokens[$stackPtr]['line']) { 56 | $i++; 57 | break; 58 | } 59 | } 60 | 61 | if ($tokens[$i]['code'] === T_WHITESPACE) { 62 | $functionIndent = strlen($tokens[$i]['content']); 63 | } 64 | 65 | // The closing parenthesis must be on a new line, even 66 | // when checking abstract function definitions. 67 | $closeBracket = $tokens[$stackPtr]['parenthesis_closer']; 68 | $prev = $phpcsFile->findPrevious( 69 | T_WHITESPACE, 70 | ($closeBracket - 1), 71 | null, 72 | true 73 | ); 74 | 75 | if ($tokens[$closeBracket]['line'] !== $tokens[$tokens[$closeBracket]['parenthesis_opener']]['line']) { 76 | if ($tokens[$prev]['line'] === $tokens[$closeBracket]['line']) { 77 | $error = 'The closing parenthesis of a multi-line function declaration must be on a new line'; 78 | $fix = $phpcsFile->addFixableError($error, $closeBracket, 'CloseBracketLine'); 79 | if ($fix === true) { 80 | $phpcsFile->fixer->addNewlineBefore($closeBracket); 81 | } 82 | } 83 | } 84 | 85 | // If this is a closure and is using a USE statement, the closing 86 | // parenthesis we need to look at from now on is the closing parenthesis 87 | // of the USE statement. 88 | if ($tokens[$stackPtr]['code'] === T_CLOSURE) { 89 | $use = $phpcsFile->findNext(T_USE, ($closeBracket + 1), $tokens[$stackPtr]['scope_opener']); 90 | if ($use !== false) { 91 | $open = $phpcsFile->findNext(T_OPEN_PARENTHESIS, ($use + 1)); 92 | $closeBracket = $tokens[$open]['parenthesis_closer']; 93 | 94 | $prev = $phpcsFile->findPrevious( 95 | T_WHITESPACE, 96 | ($closeBracket - 1), 97 | null, 98 | true 99 | ); 100 | 101 | if ($tokens[$closeBracket]['line'] !== $tokens[$tokens[$closeBracket]['parenthesis_opener']]['line']) { 102 | if ($tokens[$prev]['line'] === $tokens[$closeBracket]['line']) { 103 | $error = 'The closing parenthesis of a multi-line use declaration must be on a new line'; 104 | $fix = $phpcsFile->addFixableError($error, $closeBracket, 'UseCloseBracketLine'); 105 | if ($fix === true) { 106 | $phpcsFile->fixer->addNewlineBefore($closeBracket); 107 | } 108 | } 109 | } 110 | }//end if 111 | }//end if 112 | 113 | // Each line between the parenthesis should be indented 4 spaces. 114 | $openBracket = $tokens[$stackPtr]['parenthesis_opener']; 115 | $lastLine = $tokens[$openBracket]['line']; 116 | for ($i = ($openBracket + 1); $i < $closeBracket; $i++) { 117 | if ($tokens[$i]['line'] !== $lastLine) { 118 | if ($i === $tokens[$stackPtr]['parenthesis_closer'] 119 | || ($tokens[$i]['code'] === T_WHITESPACE 120 | && (($i + 1) === $closeBracket 121 | || ($i + 1) === $tokens[$stackPtr]['parenthesis_closer'])) 122 | ) { 123 | // Closing braces need to be indented to the same level 124 | // as the function. 125 | $expectedIndent = $functionIndent; 126 | } else { 127 | $expectedIndent = ($functionIndent + $this->indent); 128 | } 129 | 130 | // We changed lines, so this should be a whitespace indent token. 131 | if ($tokens[$i]['code'] !== T_WHITESPACE) { 132 | $foundIndent = 0; 133 | } else { 134 | $foundIndent = strlen($tokens[$i]['content']); 135 | } 136 | 137 | if ($expectedIndent !== $foundIndent) { 138 | $error = 'Multi-line function declaration not indented correctly; expected %s spaces but found %s'; 139 | $data = array( 140 | $expectedIndent, 141 | $foundIndent, 142 | ); 143 | 144 | $fix = $phpcsFile->addFixableError($error, $i, 'Indent', $data); 145 | if ($fix === true) { 146 | $spaces = str_repeat(' ', $expectedIndent); 147 | if ($foundIndent === 0) { 148 | $phpcsFile->fixer->addContentBefore($i, $spaces); 149 | } else { 150 | $phpcsFile->fixer->replaceToken($i, $spaces); 151 | } 152 | } 153 | } 154 | 155 | $lastLine = $tokens[$i]['line']; 156 | }//end if 157 | 158 | if ($tokens[$i]['code'] === T_ARRAY || $tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) { 159 | // Skip arrays as they have their own indentation rules. 160 | if ($tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) { 161 | $i = $tokens[$i]['bracket_closer']; 162 | } else { 163 | $i = $tokens[$i]['parenthesis_closer']; 164 | } 165 | 166 | $lastLine = $tokens[$i]['line']; 167 | continue; 168 | } 169 | }//end for 170 | 171 | if (isset($tokens[$stackPtr]['scope_opener']) === true) { 172 | 173 | // any scope opener, we get the next token (should be EOL) 174 | $next = $tokens[($closeBracket + 1)]; 175 | 176 | // if the token is EOL, then no error 177 | if ($next['content'] === $phpcsFile->eolChar) 178 | { 179 | $length = -1; 180 | } elseif ($next['code'] == T_OPEN_CURLY_BRACKET) { 181 | $length = 0; 182 | } else { 183 | $length = strlen($next['content']); 184 | } 185 | 186 | // any length means a problem, even zero 187 | if ($length >= 0) { 188 | $data = array($length); 189 | $code = 'NewLineBeforeOpenBrace'; 190 | 191 | $error = 'There must be a newline before the opening brace of a multi-line function declaration; found '; 192 | 193 | // if whitespace, then report it 194 | if ($length > 0) { 195 | $error .= '%s spaces'; 196 | $code = 'SpaceBeforeOpenBrace'; 197 | } 198 | // otherwise, no space but still brace on same line 199 | else 200 | { 201 | $error .= ' opening brace'; 202 | } 203 | 204 | $fix = $phpcsFile->addFixableError($error, ($closeBracket + 1), $code, $data); 205 | if ($fix === true) { 206 | 207 | // remove whitespace 208 | if ($length > 0) 209 | { 210 | $phpcsFile->fixer->replaceToken($closeBracket + 1, ''); 211 | } 212 | 213 | // add the EOL token 214 | $phpcsFile->fixer->addContent($closeBracket, $phpcsFile->eolChar); 215 | } 216 | 217 | return; 218 | }//end if 219 | 220 | // And just in case they do something funny before the brace... 221 | $next = $phpcsFile->findNext( 222 | T_WHITESPACE, 223 | ($closeBracket + 1), 224 | null, 225 | true 226 | ); 227 | 228 | if ($next !== false && $tokens[$next]['code'] !== T_OPEN_CURLY_BRACKET) { 229 | $error = 'There must be a single space between the closing parenthesis and the opening brace of a multi-line function declaration'; 230 | $phpcsFile->addError($error, $next, 'NoSpaceBeforeOpenBrace'); 231 | } 232 | }//end if 233 | 234 | $openBracket = $tokens[$stackPtr]['parenthesis_opener']; 235 | $this->processBracket($phpcsFile, $openBracket, $tokens, 'function'); 236 | 237 | if ($tokens[$stackPtr]['code'] !== T_CLOSURE) { 238 | return; 239 | } 240 | 241 | $use = $phpcsFile->findNext(T_USE, ($tokens[$stackPtr]['parenthesis_closer'] + 1), $tokens[$stackPtr]['scope_opener']); 242 | if ($use === false) { 243 | return; 244 | } 245 | 246 | $openBracket = $phpcsFile->findNext(T_OPEN_PARENTHESIS, ($use + 1), null); 247 | $this->processBracket($phpcsFile, $openBracket, $tokens, 'use'); 248 | 249 | // Also check spacing. 250 | if ($tokens[($use - 1)]['code'] === T_WHITESPACE) { 251 | $gap = strlen($tokens[($use - 1)]['content']); 252 | } else { 253 | $gap = 0; 254 | } 255 | 256 | }//end processMultiLineDeclaration() 257 | 258 | 259 | /** 260 | * Processes the contents of a single set of brackets. 261 | * 262 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 263 | * @param int $openBracket The position of the open bracket 264 | * in the stack passed in $tokens. 265 | * @param array $tokens The stack of tokens that make up 266 | * the file. 267 | * @param string $type The type of the token the brackets 268 | * belong to (function or use). 269 | * 270 | * @return void 271 | */ 272 | public function processBracket(PHP_CodeSniffer_File $phpcsFile, $openBracket, $tokens, $type='function') 273 | { 274 | $errorPrefix = ''; 275 | if ($type === 'use') { 276 | $errorPrefix = 'Use'; 277 | } 278 | 279 | $closeBracket = $tokens[$openBracket]['parenthesis_closer']; 280 | 281 | // The open bracket should be the last thing on the line. 282 | if ($tokens[$openBracket]['line'] !== $tokens[$closeBracket]['line']) { 283 | $next = $phpcsFile->findNext(T_WHITESPACE, ($openBracket + 1), null, true); 284 | if ($tokens[$next]['line'] !== ($tokens[$openBracket]['line'] + 1)) { 285 | $error = 'The first parameter of a multi-line '.$type.' declaration must be on the line after the opening bracket'; 286 | $phpcsFile->addError($error, $next, $errorPrefix.'FirstParamSpacing'); 287 | } 288 | } 289 | 290 | // Each line between the brackets should contain a single parameter. 291 | $lastCommaLine = null; 292 | for ($i = ($openBracket + 1); $i < $closeBracket; $i++) { 293 | // Skip brackets, like arrays, as they can contain commas. 294 | if (isset($tokens[$i]['parenthesis_opener']) === true) { 295 | $i = $tokens[$i]['parenthesis_closer']; 296 | continue; 297 | } 298 | 299 | if ($tokens[$i]['code'] === T_COMMA) { 300 | if ($lastCommaLine !== null && $lastCommaLine === $tokens[$i]['line']) { 301 | $error = 'Multi-line '.$type.' declarations must define one parameter per line'; 302 | $phpcsFile->addError($error, $i, $errorPrefix.'OneParamPerLine'); 303 | } else { 304 | // Comma must be the last thing on the line. 305 | $next = $phpcsFile->findNext(T_WHITESPACE, ($i + 1), null, true); 306 | if ($tokens[$next]['line'] !== ($tokens[$i]['line'] + 1)) { 307 | $error = 'Commas in multi-line '.$type.' declarations must be the last content on a line'; 308 | $phpcsFile->addError($error, $next, $errorPrefix.'ContentAfterComma'); 309 | } 310 | } 311 | 312 | $lastCommaLine = $tokens[$i]['line']; 313 | } 314 | } 315 | 316 | }//end processBracket() 317 | 318 | /** 319 | * Processes single-line declarations. 320 | * 321 | * Just uses the Generic BSD-Allman brace sniff. 322 | * 323 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 324 | * @param int $stackPtr The position of the current token 325 | * in the stack passed in $tokens. 326 | * @param array $tokens The stack of tokens that make up 327 | * the file. 328 | * 329 | * @return void 330 | */ 331 | public function processSingleLineDeclaration(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $tokens) 332 | { 333 | if (class_exists('Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff', true) === false) { 334 | throw new PHP_CodeSniffer_Exception('Class Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff not found'); 335 | } 336 | 337 | $sniff = new Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff(); 338 | 339 | $sniff->process($phpcsFile, $stackPtr); 340 | 341 | }//end processSingleLineDeclaration() 342 | 343 | 344 | }//end class 345 | 346 | ?> 347 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/bootstrap.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barracudanetworks/forkdaemon-php/496e186daee40920482bcfb9f81c19ffaa6fd0a6/.phpcs/Barracuda/bootstrap.php -------------------------------------------------------------------------------- /.phpcs/Barracuda/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PSR2 with Barracuda exceptions 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 48 | 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Barracuda Networks, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Fork Daemon 2 | A library to make setup and management of forking daemons in PHP easy. 3 | 4 | ## Features 5 | - Easy management of PHP forks 6 | - Return result of children by callback or polling parent for results 7 | - Splitting work units into buckets 8 | - Preforking callbacks to manage resources before forking 9 | - Dynamic setting of number of children / work per child 10 | 11 | ## Usage 12 | Check out the examples in the examples directory 13 | ``php example/blocking.php`` 14 | 15 | ## Caveats 16 | - You need to specify ``declare(ticks=1);`` before inclusion of the fork-daemon library, otherwise signals wont be handled. This *must* be done in the main PHP file, as ``declare(ticks=N);`` only works for the file in which it is declared and the files which that file includes. Reference: [PHP Documentation](http://php.net/manual/en/control-structures.declare.php#control-structures.declare.ticks) 17 | 18 | ## License 19 | Copyright 2013 Barracuda Networks, Inc. 20 | Licensed under the MIT License 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "barracuda/forkdaemon-php", 3 | "type": "library", 4 | "description": "A library to make setup and management of forking daemons in PHP easy.", 5 | "keywords": ["forking", "daemons", "php"], 6 | "homepage": "https://github.com/barracudanetworks/forkdaemon-php", 7 | "license": "MIT", 8 | "require": { 9 | "php": ">=5.3.0" 10 | }, 11 | "autoload": { 12 | "classmap": [ "fork_daemon.php" ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/blocking.php: -------------------------------------------------------------------------------- 1 | max_children_set(5); 10 | $server->max_work_per_child_set(3); 11 | $server->register_child_run("process_child_run"); 12 | $server->register_parent_child_exit("process_child_exit"); 13 | $server->register_logging("logger", fork_daemon::LOG_LEVEL_ALL); 14 | 15 | test_blocking(); 16 | 17 | function test_blocking() 18 | { 19 | global $server; 20 | 21 | echo "Adding 10 units of work\n"; 22 | 23 | $data_set = array(); 24 | for($i=0; $i<10; $i++) $data_set[] = $i; 25 | shuffle($data_set); 26 | $server->addwork($data_set); 27 | 28 | /* process work blocking mode */ 29 | $server->process_work(true); 30 | 31 | echo "Adding 15 more units of work\n"; 32 | 33 | $data_set = array(); 34 | for($i=10; $i<25; $i++) $data_set[] = $i; 35 | shuffle($data_set); 36 | $server->addwork($data_set); 37 | 38 | /* process work blocking mode */ 39 | $server->process_work(true); 40 | } 41 | 42 | /* 43 | * CALLBACK FUNCTIONS 44 | */ 45 | 46 | /* registered call back function */ 47 | function process_child_run($data_set, $identifier = "") 48 | { 49 | echo "I'm child working on: " . implode(",", $data_set) . ($identifier == "" ? "" : " (id:$identifier)") . "\n"; 50 | sleep(rand(4,8)); 51 | } 52 | 53 | /* registered call back function */ 54 | function process_child_exit($pid, $identifier = "") 55 | { 56 | echo "Child $pid just finished" . ($identifier == "" ? "" : " (id:$identifier)") . "\n"; 57 | } 58 | 59 | /* registered call back function */ 60 | function logger($message) 61 | { 62 | echo "logger: " . $message . PHP_EOL; 63 | } 64 | -------------------------------------------------------------------------------- /examples/bucket.php: -------------------------------------------------------------------------------- 1 | max_children_set(5); 10 | $server->max_work_per_child_set(3); 11 | $server->register_child_run("process_child_run"); 12 | $server->register_parent_child_exit("process_child_exit"); 13 | $server->register_logging("logger", fork_daemon::LOG_LEVEL_ALL); 14 | 15 | test_bucket(); 16 | 17 | function test_bucket() 18 | { 19 | global $server; 20 | 21 | define("BUCKET1", 1); 22 | define("BUCKET2", 2); 23 | 24 | $server->add_bucket(BUCKET1); 25 | $server->add_bucket(BUCKET2); 26 | $server->max_children_set(2, BUCKET1); 27 | $server->max_children_set(5, BUCKET2); 28 | 29 | $data_set = array(); 30 | for($i=0; $i<100; $i++) $data_set[] = $i; 31 | 32 | /* add work to bucket 1 */ 33 | shuffle($data_set); 34 | $server->addwork($data_set, "", BUCKET1); 35 | 36 | /* add work to bucket 2 */ 37 | shuffle($data_set); 38 | $server->addwork($data_set, "", BUCKET2); 39 | 40 | /* wait until all work allocated */ 41 | while ($server->work_sets_count(BUCKET1) > 0 || $server->work_sets_count(BUCKET2) > 0) 42 | { 43 | echo "work set count(1): " . $server->work_sets_count(BUCKET1) . ", count(2): " . $server->work_sets_count(BUCKET2) . "\n"; 44 | if ($server->work_sets_count(BUCKET1) > 0) $server->process_work(false, BUCKET1); 45 | if ($server->work_sets_count(BUCKET2) > 0) $server->process_work(false, BUCKET2); 46 | sleep(1); 47 | } 48 | 49 | /* wait until all children finish */ 50 | while ($server->children_running() > 0) 51 | { 52 | echo "waiting for " . $server->children_running() . " children to finish\n"; 53 | sleep(1); 54 | } 55 | } 56 | 57 | /* 58 | * CALLBACK FUNCTIONS 59 | */ 60 | 61 | /* registered call back function */ 62 | function process_child_run($data_set, $identifier = "") 63 | { 64 | echo "I'm child working on: " . implode(",", $data_set) . ($identifier == "" ? "" : " (id:$identifier)") . "\n"; 65 | sleep(rand(4,8)); 66 | } 67 | 68 | /* registered call back function */ 69 | function process_child_exit($pid, $identifier = "") 70 | { 71 | echo "Child $pid just finished" . ($identifier == "" ? "" : " (id:$identifier)") . "\n"; 72 | } 73 | 74 | /* registered call back function */ 75 | function logger($message) 76 | { 77 | echo "logger: " . $message . PHP_EOL; 78 | } 79 | -------------------------------------------------------------------------------- /examples/identifier.php: -------------------------------------------------------------------------------- 1 | max_children_set(5); 10 | $server->max_work_per_child_set(3); 11 | $server->register_child_run("process_child_run"); 12 | $server->register_parent_child_exit("process_child_exit"); 13 | $server->register_logging("logger", fork_daemon::LOG_LEVEL_ALL); 14 | 15 | test_identifier(); 16 | 17 | function test_identifier() 18 | { 19 | global $server; 20 | 21 | $server->child_single_work_item_set(true); 22 | $server->max_work_per_child_set(1); 23 | 24 | echo "Adding 100 units of work\n"; 25 | 26 | /* add work */ 27 | $data_set = array(); 28 | for($i=0; $i<100; $i++) $data_set[] = $i; 29 | shuffle($data_set); 30 | $data_set = array_chunk($data_set, 3); 31 | 32 | $i = 0; 33 | foreach ($data_set as $item) 34 | { 35 | $server->addwork($item, "IDn$i"); 36 | $i++; 37 | } 38 | 39 | echo "Processing work in non-blocking mode\n"; 40 | 41 | /* process work non blocking mode */ 42 | $server->process_work(false); 43 | 44 | /* wait until all work allocated */ 45 | while ($server->work_sets_count() > 0) 46 | { 47 | echo "work set count: " . $server->work_sets_count() . "\n"; 48 | $server->process_work(false); 49 | sleep(1); 50 | } 51 | 52 | /* wait until all children finish */ 53 | while ($server->children_running() > 0) 54 | { 55 | echo "waiting for " . $server->children_running() . " children to finish\n"; 56 | sleep(1); 57 | } 58 | } 59 | 60 | /* 61 | * CALLBACK FUNCTIONS 62 | */ 63 | 64 | /* registered call back function */ 65 | function process_child_run($data_set, $identifier = "") 66 | { 67 | echo "I'm child working on: " . implode(",", $data_set) . ($identifier == "" ? "" : " (id:$identifier)") . "\n"; 68 | sleep(rand(4,8)); 69 | } 70 | 71 | /* registered call back function */ 72 | function process_child_exit($pid, $identifier = "") 73 | { 74 | echo "Child $pid just finished" . ($identifier == "" ? "" : " (id:$identifier)") . "\n"; 75 | } 76 | 77 | /* registered call back function */ 78 | function logger($message) 79 | { 80 | echo "logger: " . $message . PHP_EOL; 81 | } 82 | -------------------------------------------------------------------------------- /examples/nonblocking.php: -------------------------------------------------------------------------------- 1 | max_children_set(5); 10 | $server->max_work_per_child_set(3); 11 | $server->register_child_run("process_child_run"); 12 | $server->register_parent_child_exit("process_child_exit"); 13 | $server->register_logging("logger", fork_daemon::LOG_LEVEL_ALL); 14 | 15 | test_nonblocking(); 16 | 17 | function test_nonblocking() 18 | { 19 | global $server; 20 | 21 | echo "Adding 100 units of work\n"; 22 | 23 | /* add work */ 24 | $data_set = array(); 25 | for($i=0; $i<100; $i++) $data_set[] = $i; 26 | shuffle($data_set); 27 | $server->addwork($data_set); 28 | 29 | echo "Processing work in non-blocking mode\n"; 30 | 31 | /* process work non blocking mode */ 32 | $server->process_work(false); 33 | 34 | /* wait until all work allocated */ 35 | while ($server->work_sets_count() > 0) 36 | { 37 | echo "work set count: " . $server->work_sets_count() . "\n"; 38 | $server->process_work(false); 39 | sleep(1); 40 | } 41 | 42 | /* wait until all children finish */ 43 | while ($server->children_running() > 0) 44 | { 45 | echo "waiting for " . $server->children_running() . " children to finish\n"; 46 | sleep(1); 47 | } 48 | } 49 | 50 | /* 51 | * CALLBACK FUNCTIONS 52 | */ 53 | 54 | /* registered call back function */ 55 | function process_child_run($data_set, $identifier = "") 56 | { 57 | echo "I'm child working on: " . implode(",", $data_set) . ($identifier == "" ? "" : " (id:$identifier)") . "\n"; 58 | sleep(rand(4,8)); 59 | } 60 | 61 | /* registered call back function */ 62 | function process_child_exit($pid, $identifier = "") 63 | { 64 | echo "Child $pid just finished" . ($identifier == "" ? "" : " (id:$identifier)") . "\n"; 65 | } 66 | 67 | /* registered call back function */ 68 | function logger($message) 69 | { 70 | echo "logger: " . $message . PHP_EOL; 71 | } 72 | -------------------------------------------------------------------------------- /examples/result_callback.php: -------------------------------------------------------------------------------- 1 | max_children_set(5); 14 | $server->max_work_per_child_set(3); 15 | $server->register_child_run("process_child_run"); 16 | $server->register_parent_child_exit("process_child_exit"); 17 | $server->register_logging("logger", fork_daemon::LOG_LEVEL_ALL); 18 | $server->register_parent_results("process_results"); 19 | 20 | test_nonblocking(); 21 | 22 | function test_nonblocking() 23 | { 24 | global $server; 25 | 26 | echo "Adding 100 units of work\n"; 27 | 28 | /* add work */ 29 | $data_set = array(); 30 | for($i=0; $i<100; $i++) $data_set[] = $i; 31 | shuffle($data_set); 32 | $server->addwork($data_set); 33 | 34 | echo "Processing work in non-blocking mode\n"; 35 | 36 | /* process work non blocking mode */ 37 | $server->process_work(false); 38 | 39 | /* wait until all work allocated */ 40 | while ($server->work_sets_count() > 0) 41 | { 42 | echo "work set count: " . $server->work_sets_count() . "\n"; 43 | $server->process_work(false); 44 | sleep(1); 45 | } 46 | 47 | /* wait until all children finish */ 48 | while ($server->children_running() > 0) 49 | { 50 | echo "waiting for " . $server->children_running() . " children to finish\n"; 51 | sleep(1); 52 | } 53 | } 54 | 55 | /* 56 | * CALLBACK FUNCTIONS 57 | */ 58 | 59 | function process_results($results, $identifier = "") 60 | { 61 | echo "Results came back: " . implode(",", $results) . ($identifier == "" ? "" : " (id:$identifier)") . PHP_EOL; 62 | } 63 | 64 | /* registered call back function */ 65 | function process_child_run($data_set, $identifier = "") 66 | { 67 | echo "I'm child working on: " . implode(",", $data_set) . ($identifier == "" ? "" : " (id:$identifier)") . "\n"; 68 | 69 | $result = array_sum($data_set); 70 | sleep(rand(1,3)); 71 | 72 | // return results 73 | return $result; 74 | } 75 | 76 | /* registered call back function */ 77 | function process_child_exit($pid, $identifier = "") 78 | { 79 | echo "Child $pid just finished" . ($identifier == "" ? "" : " (id:$identifier)") . "\n"; 80 | } 81 | 82 | /* registered call back function */ 83 | function logger($message) 84 | { 85 | echo "logger: " . $message . PHP_EOL; 86 | } 87 | -------------------------------------------------------------------------------- /examples/result_deferred.php: -------------------------------------------------------------------------------- 1 | max_children_set(100); 15 | $server->max_work_per_child_set(3); 16 | $server->store_result_set(true); 17 | $server->register_child_run("process_child_run"); 18 | $server->register_parent_child_exit("process_child_exit"); 19 | $server->register_logging("logger", fork_daemon::LOG_LEVEL_ALL); 20 | // no callback with this method since we check results at the end 21 | 22 | test_nonblocking(); 23 | 24 | // since deferred results, check at the end 25 | $results = $server->get_all_results(); 26 | var_dump($results); 27 | echo "Sum: " . array_sum($results) . PHP_EOL; 28 | echo "Count: " . count($results) . PHP_EOL; 29 | 30 | function test_nonblocking() 31 | { 32 | global $server; 33 | 34 | echo "Adding 100 units of work\n"; 35 | 36 | /* add work */ 37 | $data_set = array(); 38 | for($i=0; $i<100; $i++) $data_set[] = $i; 39 | shuffle($data_set); 40 | $server->addwork($data_set); 41 | 42 | echo "Processing work in non-blocking mode\n"; 43 | echo "Sum: " . array_sum($data_set) . PHP_EOL; 44 | 45 | /* process work non blocking mode */ 46 | $server->process_work(false); 47 | 48 | /* wait until all work allocated */ 49 | while ($server->work_sets_count() > 0) 50 | { 51 | echo "work set count: " . $server->work_sets_count() . "\n"; 52 | $server->process_work(false); 53 | sleep(1); 54 | } 55 | 56 | /* wait until all children finish */ 57 | while ($server->children_running() > 0) 58 | { 59 | echo "waiting for " . $server->children_running() . " children to finish\n"; 60 | sleep(1); 61 | } 62 | } 63 | 64 | /* 65 | * CALLBACK FUNCTIONS 66 | */ 67 | 68 | /* registered call back function */ 69 | function process_child_run($data_set, $identifier = "") 70 | { 71 | echo "I'm child working on: " . implode(",", $data_set) . ($identifier == "" ? "" : " (id:$identifier)") . "\n"; 72 | 73 | sleep(rand(1,3)); 74 | 75 | // just do a sum and return it as the result 76 | $result = array_sum($data_set); 77 | 78 | // return results 79 | return $result; 80 | } 81 | 82 | /* registered call back function */ 83 | function process_child_exit($pid, $identifier = "") 84 | { 85 | echo "Child $pid just finished" . ($identifier == "" ? "" : " (id:$identifier)") . "\n"; 86 | } 87 | 88 | /* registered call back function */ 89 | function logger($message) 90 | { 91 | echo "logger: " . $message . PHP_EOL; 92 | } 93 | -------------------------------------------------------------------------------- /fork_daemon.php: -------------------------------------------------------------------------------- 1 | 86400); 51 | 52 | /** 53 | * Whether the bucket is in persisent mode or not. 54 | * 55 | * Persistent mode will keep $max_children[$bucket] processes running permanently 56 | * 57 | * @var array 58 | */ 59 | protected $child_persistent_mode = array(self::DEFAULT_BUCKET => false); 60 | 61 | /** 62 | * The data that is passed to the child functions when in persistent mode 63 | * 64 | * @var array 65 | */ 66 | protected $child_persistent_mode_data = array(self::DEFAULT_BUCKET => null); 67 | 68 | /** 69 | * Function the child invokes with a set of worker units 70 | * @access protected 71 | * @var integer $child_function_run 72 | */ 73 | protected $child_function_run = array(self::DEFAULT_BUCKET => ''); 74 | 75 | /** 76 | * Function the parent invokes when a child finishes 77 | * @access protected 78 | * @var integer $parent_function_child_exited 79 | */ 80 | protected $parent_function_child_exited = array(self::DEFAULT_BUCKET => ''); 81 | 82 | /** 83 | * Function the child invokes when sigint/term is received 84 | * @access protected 85 | * @var integer $child_function_exit 86 | */ 87 | protected $child_function_exit = array(self::DEFAULT_BUCKET => ''); 88 | 89 | /** 90 | * Function the parent invokes when a child is killed due to exceeding the max runtime 91 | * @access protected 92 | * @var integer $child_function_timeout 93 | */ 94 | protected $child_function_timeout = array(self::DEFAULT_BUCKET => ''); 95 | 96 | /** 97 | * Function the parent invokes before forking a child 98 | * @access protected 99 | * @var integer $parent_function_prefork 100 | */ 101 | protected $parent_function_prefork = ''; 102 | 103 | /** 104 | * Function the parent invokes when a child is spawned 105 | * @access protected 106 | * @var integer $parent_function_fork 107 | */ 108 | protected $parent_function_fork = array(self::DEFAULT_BUCKET => ''); 109 | 110 | /** 111 | * Function the parent invokes when the parent receives a SIGHUP 112 | * @access protected 113 | * @var integer $parent_function_sighup 114 | */ 115 | protected $parent_function_sighup = ''; 116 | 117 | /** 118 | * Property of the parent sighup function. If true, the parent 119 | * will send sighup to all children when the parent receives a 120 | * sighup. 121 | * @access protected 122 | * @var integer $parent_function_sighup_cascade 123 | */ 124 | protected $parent_function_sighup_cascade = true; 125 | 126 | /** 127 | * Function the child invokes when the child receives a SIGHUP 128 | * @access protected 129 | * @var integer $child_function_sighup 130 | */ 131 | protected $child_function_sighup = array(self::DEFAULT_BUCKET => ''); 132 | 133 | /** 134 | * Function the parent invokes when a child has results to post 135 | * @access protected 136 | * @var integer $parent_function_results 137 | */ 138 | protected $parent_function_results = array(self::DEFAULT_BUCKET => ''); 139 | 140 | /** 141 | * Stores whether results are stored for retrieval by the parent 142 | * @access protected 143 | * @var boolean $store_result 144 | */ 145 | protected $store_result = false; 146 | 147 | /** 148 | * Max number of seconds to wait for a child process 149 | * exit once it has been requested to exit 150 | * @access protected 151 | * @var integer $children_kill_timeout 152 | */ 153 | protected $children_max_timeout = 30; 154 | 155 | /** 156 | * Function the parent runs when the daemon is getting shutdown 157 | * @access protected 158 | * @var integer $parent_function_exit 159 | */ 160 | protected $parent_function_exit = ''; 161 | 162 | /** 163 | * Stores whether the daemon is in single item mode or not 164 | * @access protected 165 | * @var bool $child_single_work_item 166 | */ 167 | protected $child_single_work_item = array(self::DEFAULT_BUCKET => false); 168 | 169 | /** 170 | * Function to call when there is a message to log 171 | * @access protected 172 | * @var array $log_function array of callables index by severity 173 | * called with call_user_func($log_function, $message) 174 | */ 175 | protected $log_function = null; 176 | 177 | /** 178 | * Stores whether or not we have received an exit request 179 | * @access protected 180 | * @default false 181 | * @var bool $exit_request_status 182 | */ 183 | protected $exit_request_status = false; 184 | 185 | // **************** SERVER CONTROLS ****************/ 186 | /** 187 | * Upper limit on the number of children started. 188 | * @access protected 189 | * @var integer $max_children 190 | */ 191 | protected $max_children = array(self::DEFAULT_BUCKET => 25); 192 | 193 | /** 194 | * Upper limit on the number of work units sent to each child. 195 | * @access protected 196 | * @var integer $max_work_per_child 197 | */ 198 | protected $max_work_per_child = array(self::DEFAULT_BUCKET => 100); 199 | 200 | /** 201 | * Interval to do house keeping in seconds 202 | * @access protected 203 | * @var integer $housekeeping_check_interval 204 | */ 205 | protected $housekeeping_check_interval = 20; 206 | 207 | // **************** TRACKING CONTROLS ****************/ 208 | 209 | /** 210 | * track children of parent including their status and create time 211 | * @access protected 212 | * @var array $forked_children 213 | */ 214 | protected $forked_children = array(); 215 | 216 | /** 217 | * number of tracked children (not stopped) 218 | * @access protected 219 | * @var array $forked_children_count 220 | */ 221 | protected $forked_children_count = 0; 222 | 223 | /** 224 | * track the work units to process 225 | * @access protected 226 | * @var array $work_units 227 | */ 228 | protected $work_units = array(self::DEFAULT_BUCKET => array()); 229 | 230 | /** 231 | * track the buckets 232 | * @access protected 233 | * @var array $buckets 234 | */ 235 | protected $buckets = array(0 => self::DEFAULT_BUCKET); 236 | 237 | /** 238 | * for the parent the track the results received from chilren 239 | * @access protected 240 | * @var array $work_units 241 | */ 242 | protected $results = array(self::DEFAULT_BUCKET => array()); 243 | 244 | /** 245 | * within a child, track the bucket the child exists in. note, 246 | * this shouldn't be set or referenced in the parent process 247 | * @access protected 248 | * @var int $child_bucket 249 | */ 250 | protected $child_bucket = null; 251 | 252 | // **************** MOST IMPORTANT CONTROLS ****************/ 253 | 254 | /** 255 | * parent pid 256 | * @access protected 257 | * @var array $parent_pid 258 | */ 259 | static protected $parent_pid; 260 | 261 | /** 262 | * last housekeeping check time 263 | * @access protected 264 | * @var array $housekeeping_last_check 265 | */ 266 | protected $housekeeping_last_check = 0; 267 | 268 | /** 269 | * Whether to free unused memory in the child process after fork. 270 | * Setting this to false can avoid an expensive garbage collection 271 | * in situations with a short-lived child process and a large work 272 | * list. By default, free this memory. 273 | * @access protected 274 | * @var bool $free_unused_memory_in_child 275 | */ 276 | protected $free_unused_memory_in_child = true; 277 | 278 | // **************** FUNCTION DEFINITIONS ****************/ 279 | 280 | /** 281 | * Set and Get functions 282 | */ 283 | 284 | /** 285 | * @return array The forked children array. 286 | */ 287 | public function getForkedChildren() 288 | { 289 | return $this->forked_children; 290 | } 291 | 292 | /** 293 | * Calling this function will prevent the child from freeing 294 | * the work_list and other work tracking variables. Normally 295 | * these are freed immediately since the forked child process 296 | * has no need of them, but in some cases (like when the child 297 | * calls pcntl_exec after forking) the triggered garbage 298 | * collection may negatively impact performance with no benefit. 299 | */ 300 | public function disable_child_memory_cleanup() 301 | { 302 | $this->free_unused_memory_in_child = false; 303 | } 304 | 305 | /** 306 | * Allows the app to set the max_children value 307 | * @access public 308 | * @param int $value The new max_children value. 309 | * @param int $bucket The bucket to use. 310 | * @return void 311 | */ 312 | public function max_children_set($value, $bucket = self::DEFAULT_BUCKET) 313 | { 314 | if ($value < 1) 315 | { 316 | $value = 0; 317 | $this->log(($bucket === self::DEFAULT_BUCKET ? 'default' : $bucket) . ' bucket max_children set to 0, bucket will be disabled', self::LOG_LEVEL_WARN); 318 | } 319 | 320 | $old = $this->max_children[$bucket]; 321 | $this->max_children[$bucket] = $value; 322 | 323 | if ($this->child_persistent_mode[$bucket] && $old > $this->max_children[$bucket]) 324 | { 325 | $difference = $old - $this->max_children[$bucket]; 326 | $killed = 0; 327 | // Kill some of the child processes 328 | foreach ($this->forked_children as $pid => $child) 329 | { 330 | if ($child['bucket'] == $bucket && $child['status'] == self::WORKER) 331 | { 332 | $this->safe_kill($pid, SIGINT, "max_children lowered for bucket $bucket, killing pid $pid"); 333 | $killed++; 334 | 335 | if ($killed == $difference) 336 | { 337 | break; 338 | } 339 | } 340 | } 341 | } 342 | } 343 | 344 | /** 345 | * Allows the app to retrieve the current max_children value. 346 | * @access public 347 | * @param int $bucket The bucket to use. 348 | * @return int the max_children value 349 | */ 350 | public function max_children_get($bucket = self::DEFAULT_BUCKET) 351 | { 352 | return($this->max_children[$bucket]); 353 | } 354 | 355 | /** 356 | * Allows the app to set the max_work_per_child value 357 | * @access public 358 | * @param int $value New max_work_per_child value. 359 | * @param int $bucket The bucket to use. 360 | * @return void 361 | */ 362 | public function max_work_per_child_set($value, $bucket = self::DEFAULT_BUCKET) 363 | { 364 | if ($this->child_single_work_item[$bucket]) 365 | { 366 | $value = 1; 367 | } 368 | 369 | if ($value < 1) 370 | { 371 | $value = 0; 372 | $this->log(($bucket === self::DEFAULT_BUCKET ? 'default' : $bucket) . ' bucket max_work_per_child set to 0, bucket will be disabled', self::LOG_LEVEL_WARN); 373 | } 374 | 375 | $this->max_work_per_child[$bucket] = $value; 376 | } 377 | 378 | /** 379 | * Allows the app to retrieve the current max_work_per_child value. 380 | * @access public 381 | * @param int $bucket The bucket to use. 382 | * @return int The max_work_per_child value. 383 | */ 384 | public function max_work_per_child_get($bucket = self::DEFAULT_BUCKET) 385 | { 386 | return($this->max_work_per_child[$bucket]); 387 | } 388 | 389 | /** 390 | * Allows the app to set the child_max_run_time value 391 | * @access public 392 | * @param int $value New child_max_run_time value. 393 | * @param int $bucket The bucket to use. 394 | * @return void 395 | */ 396 | public function child_max_run_time_set($value, $bucket = self::DEFAULT_BUCKET) 397 | { 398 | if ($value == 0) 399 | { 400 | $this->log(($bucket === self::DEFAULT_BUCKET ? 'default' : $bucket) . ' bucket child_max_run_time set to 0', self::LOG_LEVEL_WARN); 401 | } 402 | elseif ($value < 0) 403 | { 404 | $value = -1; 405 | $this->log(($bucket === self::DEFAULT_BUCKET ? 'default' : $bucket) . ' bucket child_max_run_time set to unlimited', self::LOG_LEVEL_WARN); 406 | } 407 | 408 | $this->child_max_run_time[$bucket] = $value; 409 | } 410 | 411 | /** 412 | * Allows the app to retrieve the current child_max_run_time value. 413 | * @access public 414 | * @param int $bucket The bucket to use. 415 | * @return int the child_max_run_time value 416 | */ 417 | public function child_max_run_time_get($bucket = self::DEFAULT_BUCKET) 418 | { 419 | return($this->child_max_run_time[$bucket]); 420 | } 421 | 422 | /** 423 | * @param bool $persistent_mode Whether or not the bucket should be in persistent mode. 424 | * @param int $bucket The bucket to configure. 425 | * @return void 426 | */ 427 | public function child_persistent_mode_set($persistent_mode, $bucket = self::DEFAULT_BUCKET) 428 | { 429 | $this->child_persistent_mode[$bucket] = $persistent_mode; 430 | } 431 | 432 | /** 433 | * @param mixed $data The data to be passed to the child processes while in persistent mode. 434 | * @param int $bucket The bucket to configure. 435 | * @return void 436 | */ 437 | public function child_persistent_mode_data_set($data, $bucket = self::DEFAULT_BUCKET) 438 | { 439 | $this->child_persistent_mode_data[$bucket] = $data; 440 | } 441 | 442 | /** 443 | * Allows the app to set the child_single_work_item value 444 | * @access public 445 | * @param int $value New child_single_work_item value. 446 | * @param int $bucket The bucket to use. 447 | * @return void 448 | */ 449 | public function child_single_work_item_set($value, $bucket = self::DEFAULT_BUCKET) 450 | { 451 | if ($value < 1) 452 | { 453 | $value = 0; 454 | $this->log(($bucket === self::DEFAULT_BUCKET ? 'default' : $bucket) . ' bucket child_single_work_item set to 0', self::LOG_LEVEL_WARN); 455 | } 456 | 457 | $this->child_single_work_item[$bucket] = $value; 458 | } 459 | 460 | /** 461 | * Allows the app to retrieve the current child_single_work_item value. 462 | * @access public 463 | * @param int $bucket The bucket to use. 464 | * @return int the child_single_work_item value 465 | */ 466 | public function child_single_work_item_get($bucket = self::DEFAULT_BUCKET) 467 | { 468 | return($this->child_single_work_item[$bucket]); 469 | } 470 | 471 | /** 472 | * Allows the app to set the store_result value 473 | * @access public 474 | * @param int $value New store_result value. 475 | * @return void 476 | */ 477 | public function store_result_set($value) 478 | { 479 | $this->store_result = $value; 480 | } 481 | 482 | /** 483 | * Allows the app to retrieve the current store_result value. 484 | * @access public 485 | * @return boolean the store_result value 486 | */ 487 | public function store_result_get() 488 | { 489 | return $this->store_result; 490 | } 491 | 492 | /** 493 | * Allows the app to retrieve the current child_bucket value. 494 | * @access public 495 | * @return int the child_bucket value representing the bucket number of the child 496 | */ 497 | public function child_bucket_get() 498 | { 499 | // this function does not apply to the parent 500 | if (self::$parent_pid == getmypid()) 501 | { 502 | return false; 503 | } 504 | 505 | return($this->child_bucket); 506 | } 507 | 508 | 509 | /** 510 | * Creates a new bucket to house forking operations 511 | * @access public 512 | * @param int $bucket The bucket to create. 513 | * @return void 514 | */ 515 | public function add_bucket($bucket) 516 | { 517 | // create the bucket by copying values from the default bucket 518 | $this->max_children[$bucket] = $this->max_children[self::DEFAULT_BUCKET]; 519 | $this->child_single_work_item[$bucket] = $this->child_single_work_item[self::DEFAULT_BUCKET]; 520 | $this->max_work_per_child[$bucket] = $this->max_work_per_child[self::DEFAULT_BUCKET]; 521 | $this->child_max_run_time[$bucket] = $this->child_max_run_time[self::DEFAULT_BUCKET]; 522 | $this->child_single_work_item[$bucket] = $this->child_single_work_item[self::DEFAULT_BUCKET]; 523 | $this->child_function_run[$bucket] = $this->child_function_run[self::DEFAULT_BUCKET]; 524 | $this->parent_function_fork[$bucket] = $this->parent_function_fork[self::DEFAULT_BUCKET]; 525 | $this->child_function_sighup[$bucket] = $this->child_function_sighup[self::DEFAULT_BUCKET]; 526 | $this->child_function_exit[$bucket] = $this->child_function_exit[self::DEFAULT_BUCKET]; 527 | $this->child_function_timeout[$bucket] = $this->child_function_timeout[self::DEFAULT_BUCKET]; 528 | $this->parent_function_child_exited[$bucket] = $this->parent_function_child_exited[self::DEFAULT_BUCKET]; 529 | $this->child_persistent_mode[$bucket] = $this->child_persistent_mode[self::DEFAULT_BUCKET]; 530 | $this->child_persistent_mode_data[$bucket] = $this->child_persistent_mode_data[self::DEFAULT_BUCKET]; 531 | $this->work_units[$bucket] = array(); 532 | $this->buckets[$bucket] = $bucket; 533 | $this->results[$bucket] = array(); 534 | } 535 | 536 | /** 537 | * Allows the app to set the call back function for child processes 538 | * @access public 539 | * @param string $function_name Name of function to be called. 540 | * @param int $bucket The bucket to use. 541 | * @return bool true if the callback was successfully registered, false if it failed 542 | */ 543 | public function register_child_run($function_name, $bucket = self::DEFAULT_BUCKET) 544 | { 545 | // call child function 546 | if (is_callable($function_name) || ( is_array($function_name) && method_exists($function_name[0], $function_name[1]) ) || method_exists($this, $function_name) || function_exists($function_name) ) 547 | { 548 | $this->child_function_run[$bucket] = $function_name; 549 | return true; 550 | } 551 | 552 | return false; 553 | } 554 | 555 | /** 556 | * Allows the app to set call back functions to cleanup resources before forking 557 | * @access public 558 | * @param array $function_names Names of functions to be called. 559 | * @return bool true if the callback was successfully registered, false if it failed 560 | */ 561 | public function register_parent_prefork(array $function_names) 562 | { 563 | $this->parent_function_prefork = $function_names; 564 | return true; 565 | } 566 | 567 | /** 568 | * Allows the app to set the call back function for when a child process is spawned 569 | * @access public 570 | * @param string $function_name Name of function to be called. 571 | * @param int $bucket The bucket to use. 572 | * @return bool true if the callback was successfully registered, false if it failed 573 | */ 574 | public function register_parent_fork($function_name, $bucket = self::DEFAULT_BUCKET) 575 | { 576 | // call child function 577 | if ( is_callable($function_name) || ( is_array($function_name) && method_exists($function_name[0], $function_name[1]) ) || method_exists($this, $function_name) || function_exists($function_name) ) 578 | { 579 | $this->parent_function_fork[$bucket] = $function_name; 580 | return true; 581 | } 582 | 583 | return false; 584 | } 585 | 586 | /** 587 | * Allows the app to set the call back function for when a parent process receives a SIGHUP 588 | * @access public 589 | * @param string $function_name Name of function to be called. 590 | * @param bool $cascade_signal If true, the parent will send a sighup to all of it's children. 591 | * @return bool true if the callback was successfully registered, false if it failed 592 | */ 593 | public function register_parent_sighup($function_name, $cascade_signal = true) 594 | { 595 | // call child function 596 | if ( is_callable($function_name) || ( is_array($function_name) && method_exists($function_name[0], $function_name[1]) ) || method_exists($this, $function_name) || function_exists($function_name) ) 597 | { 598 | $this->parent_function_sighup = $function_name; 599 | $this->parent_function_sighup_cascade = $cascade_signal; 600 | return true; 601 | } 602 | 603 | return false; 604 | } 605 | 606 | /** 607 | * Allows the app to set the call back function for when a child process receives a SIGHUP 608 | * @access public 609 | * @param string $function_name Name of function to be called. 610 | * @param int $bucket The bucket to use. 611 | * @return bool true if the callback was successfully registered, false if it failed 612 | */ 613 | public function register_child_sighup($function_name, $bucket = self::DEFAULT_BUCKET) 614 | { 615 | // call child function 616 | if ( is_callable($function_name) || ( is_array($function_name) && method_exists($function_name[0], $function_name[1]) ) || method_exists($this, $function_name) || function_exists($function_name) ) 617 | { 618 | $this->child_function_sighup[$bucket] = $function_name; 619 | return true; 620 | } 621 | 622 | return false; 623 | } 624 | 625 | /** 626 | * Allows the app to set the call back function for when a child process exits 627 | * @access public 628 | * @param string $function_name Name of function to be called. 629 | * @param int $bucket The bucket to use. 630 | * @return bool true if the callback was successfully registered, false if it failed 631 | */ 632 | public function register_child_exit($function_name, $bucket = self::DEFAULT_BUCKET) 633 | { 634 | // call child function 635 | if ( is_callable($function_name) || ( is_array($function_name) && method_exists($function_name[0], $function_name[1]) ) || method_exists($this, $function_name) || function_exists($function_name) ) 636 | { 637 | $this->child_function_exit[$bucket] = $function_name; 638 | return true; 639 | } 640 | 641 | return false; 642 | } 643 | 644 | /** 645 | * Allows the app to set the call back function for when a child process is killed to exceeding its max runtime 646 | * @access public 647 | * @param string $function_name Name of function to be called. 648 | * @param int $bucket The bucket to use. 649 | * @return bool true if the callback was successfully registered, false if it failed 650 | */ 651 | public function register_child_timeout($function_name, $bucket = self::DEFAULT_BUCKET) 652 | { 653 | // call child function 654 | if ( is_callable($function_name) || ( is_array($function_name) && method_exists($function_name[0], $function_name[1]) ) || method_exists($this, $function_name) || function_exists($function_name) ) 655 | { 656 | $this->child_function_timeout[$bucket] = $function_name; 657 | return true; 658 | } 659 | 660 | return false; 661 | } 662 | 663 | /** 664 | * Allows the app to set the call back function for when the parent process exits 665 | * @access public 666 | * @param string $function_name Name of function to be called. 667 | * @return bool true if the callback was successfully registered, false if it failed 668 | */ 669 | public function register_parent_exit($function_name) 670 | { 671 | // call parent function 672 | if ( is_callable($function_name) || ( is_array($function_name) && method_exists($function_name[0], $function_name[1]) ) || method_exists($this, $function_name) || function_exists($function_name) ) 673 | { 674 | $this->parent_function_exit = $function_name; 675 | return true; 676 | } 677 | 678 | return false; 679 | } 680 | 681 | /** 682 | * Allows the app to set the call back function for when a child exits in the parent 683 | * @access public 684 | * @param string $function_name Name of function to be called. 685 | * @param int $bucket The bucket to use. 686 | * @return bool true if the callback was successfully registered, false if it failed 687 | */ 688 | public function register_parent_child_exit($function_name, $bucket = self::DEFAULT_BUCKET) 689 | { 690 | // call parent function 691 | if ( is_callable($function_name) || ( is_array($function_name) && method_exists($function_name[0], $function_name[1]) ) || method_exists($this, $function_name) || function_exists($function_name) ) 692 | { 693 | $this->parent_function_child_exited[$bucket] = $function_name; 694 | return true; 695 | } 696 | 697 | return false; 698 | } 699 | 700 | /** 701 | * Allows the app to set the call back function for when the a child has results 702 | * @access public 703 | * @param string $function_name Name of function to be called. 704 | * @param int $bucket The bucket to use. 705 | * @return bool true if the callback was successfully registered, false if it failed 706 | */ 707 | public function register_parent_results($function_name, $bucket = self::DEFAULT_BUCKET) 708 | { 709 | // call parent function 710 | if ( is_callable($function_name) || ( is_array($function_name) && method_exists($function_name[0], $function_name[1]) ) || method_exists($this, $function_name) || function_exists($function_name) ) 711 | { 712 | $this->parent_function_results[$bucket] = $function_name; 713 | return true; 714 | } 715 | 716 | return false; 717 | } 718 | 719 | /** 720 | * Allows the app to set the call back function for logging 721 | * @access public 722 | * @param string $function_name Name of function to be called. 723 | * @param int $severity The severity level. 724 | * @return bool true if the callback was successfully registered, false if it failed 725 | */ 726 | public function register_logging($function_name, $severity) 727 | { 728 | // call parent function 729 | if ( is_callable($function_name) || ( is_array($function_name) && method_exists($function_name[0], $function_name[1]) ) || method_exists($this, $function_name) || function_exists($function_name) ) 730 | { 731 | $this->log_function[$severity] = $function_name; 732 | return true; 733 | } 734 | 735 | return false; 736 | } 737 | 738 | // ************ NORMAL FUNCTION DEFS ************/ 739 | 740 | /** 741 | * This is the class constructor, initializes the object. 742 | * @access public 743 | */ 744 | public function __construct() 745 | { 746 | // record pid of parent process 747 | self::$parent_pid = getmypid(); 748 | 749 | // install signal handlers 750 | pcntl_async_signals(true); 751 | pcntl_signal(SIGHUP, array(&$this, 'signal_handler_sighup')); 752 | pcntl_signal(SIGCHLD, array(&$this, 'signal_handler_sigchild')); 753 | pcntl_signal(SIGTERM, array(&$this, 'signal_handler_sigint')); 754 | pcntl_signal(SIGINT, array(&$this, 'signal_handler_sigint')); 755 | pcntl_signal(SIGALRM, SIG_IGN); 756 | pcntl_signal(SIGUSR2, SIG_IGN); 757 | pcntl_signal(SIGBUS, SIG_IGN); 758 | pcntl_signal(SIGPIPE, SIG_IGN); 759 | pcntl_signal(SIGABRT, SIG_IGN); 760 | pcntl_signal(SIGFPE, SIG_IGN); 761 | pcntl_signal(SIGILL, SIG_IGN); 762 | pcntl_signal(SIGQUIT, SIG_IGN); 763 | pcntl_signal(SIGTRAP, SIG_IGN); 764 | pcntl_signal(SIGSYS, SIG_IGN); 765 | 766 | // add barracuda specific prefork functions (doesn't hurt anything) 767 | $this->parent_function_prefork = array('db_clear_connection_cache', 'memcache_clear_connection_cache'); 768 | } 769 | 770 | /** 771 | * Destructor does not do anything. 772 | * @access public 773 | */ 774 | public function __destruct() 775 | { 776 | } 777 | 778 | /** 779 | * Handle both parent and child registered sighup callbacks. 780 | * 781 | * @param int $signal_number Is the signal that called this function. (should be '1' for SIGHUP). 782 | * @return void 783 | * @access public 784 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) $signal_number is passed into all signal handlers. 785 | */ 786 | public function signal_handler_sighup($signal_number) 787 | { 788 | if (self::$parent_pid == getmypid()) 789 | { 790 | // parent received sighup 791 | $this->log('parent process [' . getmypid() . '] received sighup', self::LOG_LEVEL_DEBUG); 792 | 793 | // call parent's sighup registered callback 794 | $this->invoke_callback($this->parent_function_sighup, array(), true); 795 | 796 | // if cascading, send sighup to all child processes 797 | if ($this->parent_function_sighup_cascade === true) 798 | { 799 | foreach ($this->forked_children as $pid => $pid_info) 800 | { 801 | if ($pid_info['status'] == self::STOPPED) 802 | { 803 | continue; 804 | } 805 | $this->safe_kill($pid, SIGHUP, 'parent process [' . getmypid() . '] sending sighup to child ' . $pid, self::LOG_LEVEL_DEBUG); 806 | } 807 | } 808 | } 809 | else 810 | { 811 | // child received sighup. note a child is only in one bucket, do not loop through all buckets 812 | if (isset($this->child_bucket) && isset($this->child_function_sighup[$this->child_bucket])) 813 | { 814 | $this->log('child process [' . getmypid() . '] received sighup with bucket type [' . $this->child_bucket . ']', self::LOG_LEVEL_DEBUG); 815 | $this->invoke_callback( 816 | $this->child_function_sighup[$this->child_bucket], 817 | array($this->child_bucket), 818 | true 819 | ); 820 | } 821 | } 822 | } 823 | 824 | /** 825 | * Handle parent registered sigchild callbacks. 826 | * 827 | * @param int $signal_number Is the signal that called this function. 828 | * @return void 829 | * @access public 830 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) $signal_number is passed into all signal handlers. 831 | */ 832 | public function signal_handler_sigchild($signal_number) 833 | { 834 | // do not allow signals to interrupt this 835 | pcntl_async_signals(false); 836 | { 837 | // reap all child zombie processes 838 | if (self::$parent_pid == getmypid()) 839 | { 840 | $status = ''; 841 | 842 | do 843 | { 844 | // get child pid that exited 845 | $child_pid = pcntl_waitpid(0, $status, WNOHANG); 846 | if ($child_pid > 0) 847 | { 848 | // child exited 849 | $identifier = false; 850 | if (!isset($this->forked_children[$child_pid])) 851 | { 852 | $this->log("Cannot find $child_pid in array! (This may be a subprocess not in our fork)", self::LOG_LEVEL_INFO); 853 | continue; 854 | } 855 | 856 | $child = $this->forked_children[$child_pid]; 857 | $identifier = $child['identifier']; 858 | 859 | // call exit function if and only if its declared */ 860 | if ($child['status'] == self::WORKER) 861 | { 862 | $this->invoke_callback($this->parent_function_child_exited[ $this->forked_children[$child_pid]['bucket'] ], array($child_pid, $this->forked_children[$child_pid]['identifier']), true); 863 | } 864 | 865 | // stop the child pid 866 | $this->forked_children[$child_pid]['status'] = self::STOPPED; 867 | $this->forked_children_count--; 868 | 869 | // respawn helper processes 870 | if ($child['status'] == self::HELPER && $child['respawn'] === true) 871 | { 872 | $this->log('Helper process ' . $child_pid . ' died, respawning', self::LOG_LEVEL_INFO); 873 | $this->helper_process_spawn($child['function'], $child['arguments'], $child['identifier'], true); 874 | } 875 | 876 | // Poll for results from any children 877 | $this->post_results($child['bucket']); 878 | } 879 | elseif ($child_pid < 0) 880 | { 881 | // ignore acceptable error 'No child processes' given we force this signal to run potentially when no children exist 882 | if (pcntl_get_last_error() == 10) 883 | { 884 | continue; 885 | } 886 | 887 | // pcntl_wait got an error 888 | $this->log('pcntl_waitpid failed with error ' . pcntl_get_last_error() . ':' . pcntl_strerror((pcntl_get_last_error())), self::LOG_LEVEL_DEBUG); 889 | } 890 | } while ($child_pid > 0); 891 | } 892 | } 893 | } 894 | 895 | /** 896 | * Handle both parent and child registered sigint callbacks 897 | * 898 | * User terminated by CTRL-C (detected only by the parent) 899 | * 900 | * @param int $signal_number Is the signal that called this function. 901 | * @return void 902 | * @access public 903 | */ 904 | public function signal_handler_sigint($signal_number) 905 | { 906 | // log that we received an exit request 907 | $this->received_exit_request(true); 908 | 909 | // kill child processes 910 | if (self::$parent_pid == getmypid()) 911 | { 912 | foreach ($this->forked_children as $pid => &$pid_info) 913 | { 914 | if ($pid_info['status'] == self::STOPPED) 915 | { 916 | continue; 917 | } 918 | 919 | // tell helpers not to respawn 920 | if ($pid_info['status'] == self::HELPER) 921 | { 922 | $pid_info['respawn'] = false; 923 | } 924 | 925 | $this->safe_kill($pid, SIGINT, 'requesting child exit for pid: ' . $pid, self::LOG_LEVEL_INFO); 926 | } 927 | 928 | sleep(1); 929 | 930 | // checking for missed sigchild 931 | $this->signal_handler_sigchild(SIGCHLD); 932 | 933 | $start_time = time(); 934 | 935 | // wait for child processes to go away 936 | while ($this->forked_children_count > 0) 937 | { 938 | if (time() > ($start_time + $this->children_max_timeout)) 939 | { 940 | foreach ($this->forked_children as $pid => $child) 941 | { 942 | if ($child['status'] == self::STOPPED) 943 | { 944 | continue; 945 | } 946 | 947 | $this->safe_kill($pid, SIGKILL, 'force killing child pid: ' . $pid, self::LOG_LEVEL_INFO); 948 | 949 | // stop the child 950 | $this->forked_children[$pid]['status'] = self::STOPPED; 951 | $this->forked_children_count--; 952 | } 953 | } 954 | else 955 | { 956 | $this->log('waiting ' . ($start_time + $this->children_max_timeout - time()) . ' seconds for ' . $this->forked_children_count . ' children to clean up', self::LOG_LEVEL_INFO); 957 | sleep(1); 958 | $this->housekeeping_check(); 959 | } 960 | } 961 | 962 | // make call back to parent exit function if it exists 963 | $this->invoke_callback($this->parent_function_exit, $parameters = array(self::$parent_pid, $signal_number), true); 964 | } 965 | else 966 | { 967 | // invoke child cleanup callback 968 | if (isset($this->child_bucket)) 969 | { 970 | $this->invoke_callback($this->child_function_exit[$this->child_bucket], $parameters = array($this->child_bucket), true); 971 | } 972 | } 973 | 974 | exit(-1); 975 | } 976 | 977 | /** 978 | * Check or set if we have recieved an exit request 979 | * 980 | * @param bool $requested Have we received the request? (optional). 981 | * @return current exit request status 982 | */ 983 | public function received_exit_request($requested = null) 984 | { 985 | // if we are retreiving the value of the exit request 986 | if ($requested === null) 987 | { 988 | return $this->exit_request_status; 989 | } 990 | 991 | // ensure we have good data, or set to false if not 992 | if (!is_bool($requested)) 993 | { 994 | $requested = false; 995 | } 996 | 997 | // set and return the ne value 998 | return ($this->exit_request_status = $requested); 999 | } 1000 | 1001 | /** 1002 | * Add work to the group of work to be processed 1003 | * 1004 | * @param array $new_work_units Array of items to be handed back to child in chunks. 1005 | * @param string $identifier A unique identifier for this work. 1006 | * @param int $bucket The bucket to use. 1007 | * @param bool $sort_queue True to sort the work unit queue. 1008 | * @return void 1009 | */ 1010 | public function addwork(array $new_work_units, $identifier = '', $bucket = self::DEFAULT_BUCKET, $sort_queue = false) 1011 | { 1012 | // ensure bucket is setup before we try to add data to it 1013 | if (!array_key_exists($bucket, $this->work_units)) 1014 | { 1015 | $this->add_bucket($bucket); 1016 | } 1017 | 1018 | // add to queue to send 1019 | if ($this->child_single_work_item[$bucket]) 1020 | { 1021 | // prepend identifier with 'id-' because array_splice() re-arranges numeric keys 1022 | $this->work_units[$bucket]['id-' . $identifier] = $new_work_units; 1023 | } 1024 | elseif ($new_work_units === null || sizeof($new_work_units) === 0) 1025 | { 1026 | // no work 1027 | } 1028 | else 1029 | { 1030 | // merge in the new work units 1031 | $this->work_units[$bucket] = array_merge($this->work_units[$bucket], $new_work_units); 1032 | } 1033 | 1034 | // sort the queue 1035 | if ($sort_queue) 1036 | { 1037 | ksort($this->work_units[$bucket]); 1038 | } 1039 | 1040 | return; 1041 | } 1042 | 1043 | /** 1044 | * Based on identifier and bucket is a child working on the work 1045 | * 1046 | * @param string $identifier Unique identifier for the work. 1047 | * @param int $bucket The bucket. 1048 | * @return bool true if child has work, false if not 1049 | */ 1050 | public function is_work_running($identifier, $bucket = self::DEFAULT_BUCKET) 1051 | { 1052 | foreach ($this->forked_children as $info) 1053 | { 1054 | if (($info['status'] != self::STOPPED) && ($info['identifier'] == $identifier) && ($info['bucket'] == $bucket)) 1055 | { 1056 | return true; 1057 | } 1058 | } 1059 | 1060 | return false; 1061 | } 1062 | 1063 | /** 1064 | * Return array of currently running children 1065 | * 1066 | * @param int $bucket The bucket. 1067 | * @return bool true if child has work, false if not 1068 | */ 1069 | public function work_running($bucket = self::DEFAULT_BUCKET) 1070 | { 1071 | $results = array(); 1072 | foreach ($this->forked_children as $pid => $child) 1073 | { 1074 | if ($child['status'] != self::STOPPED && $child['bucket'] === $bucket) 1075 | { 1076 | $results[$pid] = $child; 1077 | } 1078 | } 1079 | 1080 | return $results; 1081 | } 1082 | 1083 | /** 1084 | * Return a list of the buckets which have been created 1085 | * 1086 | * @param bool $include_default_bucket Optionally include self::DEFAULT_BUCKET in returned value (DEFAULT: true). 1087 | * @return array list of buckets 1088 | */ 1089 | public function bucket_list($include_default_bucket = true) 1090 | { 1091 | $bucket_list = array(); 1092 | 1093 | foreach ($this->buckets as $bucket_id) 1094 | { 1095 | // skip the default bucket if ignored 1096 | if ( ($include_default_bucket === false) && ($bucket_id === self::DEFAULT_BUCKET) ) 1097 | { 1098 | continue; 1099 | } 1100 | 1101 | $bucket_list[] = $bucket_id; 1102 | } 1103 | 1104 | return $bucket_list; 1105 | } 1106 | 1107 | /** 1108 | * Check to see if a bucket exists 1109 | * 1110 | * @param int $bucket_id The bucket id. 1111 | * @return bool true if the bucket exists, false if it does not 1112 | */ 1113 | public function bucket_exists($bucket_id) 1114 | { 1115 | return (array_key_exists($bucket_id, $this->buckets)); 1116 | } 1117 | 1118 | /** 1119 | * Return the number of work sets queued 1120 | * 1121 | * A work set is a chunk of items to be worked on. A whole work set 1122 | * is handed off to a child processes. This size of the work sets can 1123 | * be controlled by $this->max_work_per_child_set() 1124 | * 1125 | * @param int $bucket The bucket to use. 1126 | * @param bool $process_all_buckets If set to true, return the count of all buckets. 1127 | * @return int the number of work sets queued 1128 | */ 1129 | public function work_sets_count($bucket = self::DEFAULT_BUCKET, $process_all_buckets = false) 1130 | { 1131 | // if asked to process all buckets, count all of them and return the count 1132 | if ($process_all_buckets === true) 1133 | { 1134 | $count = 0; 1135 | foreach ($this->buckets as $bucket_slot) 1136 | { 1137 | $count += count($this->work_units[$bucket_slot]); 1138 | } 1139 | return $count; 1140 | } 1141 | 1142 | return count($this->work_units[$bucket]); 1143 | } 1144 | 1145 | /** 1146 | * Return the contents of work sets queued 1147 | * 1148 | * A work set is a chunk of items to be worked on. A whole work set 1149 | * is handed off to a child processes. This size of the work sets can 1150 | * be controlled by $this->max_work_per_child_set() 1151 | * 1152 | * @param int $bucket The bucket to use. 1153 | * @return array contents of the bucket 1154 | */ 1155 | public function work_sets($bucket = self::DEFAULT_BUCKET) 1156 | { 1157 | return $this->work_units[$bucket]; 1158 | } 1159 | 1160 | /** 1161 | * Return the number of children running 1162 | * 1163 | * @param int $bucket The bucket to use. 1164 | * @param bool $show_pending True to show children that are done, 1165 | * but not yet had their results retrieved. 1166 | * @return int the number of children running 1167 | */ 1168 | public function children_running($bucket = self::DEFAULT_BUCKET, $show_pending = false) 1169 | { 1170 | // force reaping of children 1171 | $this->signal_handler_sigchild(SIGCHLD); 1172 | 1173 | // return global count if bucket is default 1174 | if ($bucket == self::DEFAULT_BUCKET) 1175 | { 1176 | return ($show_pending ? count($this->forked_children) : $this->forked_children_count); 1177 | } 1178 | 1179 | // count within the specified bucket 1180 | $count = 0; 1181 | foreach ($this->forked_children as $child) 1182 | { 1183 | if ($show_pending) 1184 | { 1185 | if ($child['bucket'] == $bucket) 1186 | { 1187 | $count++; 1188 | } 1189 | } 1190 | else if (($child['bucket'] == $bucket) && ($child['status'] != self::STOPPED)) 1191 | { 1192 | $count++; 1193 | } 1194 | } 1195 | 1196 | return $count; 1197 | } 1198 | 1199 | /** 1200 | * Returns the number of pending child items, including running children and 1201 | * work sets that have not been allocated. Children running includes those 1202 | * that have not had their results retrieved yet. 1203 | * 1204 | * @param int $bucket The bucket to check for pending children items. 1205 | * @return int Number of pending children items 1206 | */ 1207 | public function children_pending($bucket = self::DEFAULT_BUCKET) 1208 | { 1209 | return $this->children_running($bucket, true) + $this->work_sets_count($bucket); 1210 | } 1211 | 1212 | /** 1213 | * Check if the current processes is a child 1214 | * 1215 | * @return bool true if the current PID is a child PID, false otherwise 1216 | */ 1217 | public static function is_child() 1218 | { 1219 | return (isset(self::$parent_pid) ? (self::$parent_pid != getmypid()) : false); 1220 | } 1221 | 1222 | /** 1223 | * Try to kill a given process and make sure it is safe to kill 1224 | * 1225 | * @param int $pid The PID to check if it is our child. 1226 | * @param int $signal The kill signal to send to the given pid if possible. 1227 | * @param string $log_message The message to log out upon success. 1228 | * @param int $log_level The level at which to display the log_message. 1229 | * @return bool true on successful kill, false if not our child or not able to kill 1230 | */ 1231 | public function safe_kill($pid, $signal, $log_message = '', $log_level = self::LOG_LEVEL_INFO) 1232 | { 1233 | if (!array_key_exists($pid, $this->forked_children)) 1234 | { 1235 | return false; 1236 | } 1237 | 1238 | $stat_pid_file = '/proc/' . $pid . '/stat'; 1239 | if (!file_exists($stat_pid_file)) 1240 | { 1241 | $this->log('Unable to find info for PID ' . $pid . ' from ' . $stat_pid_file, self::LOG_LEVEL_DEBUG); 1242 | return false; 1243 | } 1244 | 1245 | $stat_pid_info = file_get_contents($stat_pid_file); 1246 | if ($stat_pid_info === false) 1247 | { 1248 | $this->log('Unable to get info for PID ' . $pid, self::LOG_LEVEL_DEBUG); 1249 | return false; 1250 | } 1251 | 1252 | $stat_pid_info = explode(' ', $stat_pid_info); 1253 | if (!array_key_exists(3, $stat_pid_info)) 1254 | { 1255 | $this->log('Unable to find parent PID for PID ' . $pid, self::LOG_LEVEL_DEBUG); 1256 | return false; 1257 | } 1258 | 1259 | // the parent pid is the fourth entry in /proc/PID/stat 1260 | if ($stat_pid_info[3] == getmypid()) 1261 | { 1262 | if ($log_message) 1263 | { 1264 | $this->log($log_message, $log_level); 1265 | } 1266 | 1267 | posix_kill($pid, $signal); 1268 | return true; 1269 | } 1270 | 1271 | $this->log('Failed to kill PID ' . $pid . ' with signal ' . $signal, self::LOG_LEVEL_WARN); 1272 | return false; 1273 | } 1274 | 1275 | /** 1276 | * Spawns a helper process 1277 | * 1278 | * Spawns a new helper process to perform duties under the parent server 1279 | * process without accepting connections. Helper processes can optionally 1280 | * be respawned when they die. 1281 | * 1282 | * @access public 1283 | * @param string $function_name Helper function to call. 1284 | * @param array $arguments Function arguments. 1285 | * @param string $identifier Helper process unique identifier. 1286 | * @param bool $respawn Whether to respawn the helper process when it dies. 1287 | * @return void 1288 | */ 1289 | public function helper_process_spawn($function_name, array $arguments = array(), $identifier = '', $respawn = true) 1290 | { 1291 | if ((is_array($function_name) && method_exists($function_name[0], $function_name[1])) || function_exists($function_name)) 1292 | { 1293 | // init the IPC sockets 1294 | list($socket_child, $socket_parent) = $this->ipc_init(); 1295 | 1296 | // do not process signals while we are forking 1297 | pcntl_async_signals(false); 1298 | $pid = pcntl_fork(); 1299 | 1300 | if ($pid == -1) 1301 | { 1302 | die("Forking error!\n"); 1303 | } 1304 | elseif ($pid == 0) 1305 | { 1306 | /* 1307 | * Child process 1308 | */ 1309 | 1310 | // set child properties 1311 | $this->child_bucket = self::DEFAULT_BUCKET; 1312 | 1313 | pcntl_async_signals(true); 1314 | 1315 | // close our socket (we only need the one to the parent) 1316 | socket_close($socket_child); 1317 | 1318 | // execute the function 1319 | $result = call_user_func_array($function_name, $arguments); 1320 | 1321 | // send the response to the parent 1322 | self::socket_send($socket_parent, $result); 1323 | 1324 | exit(0); 1325 | } 1326 | else 1327 | { 1328 | /* 1329 | * Parent process 1330 | */ 1331 | 1332 | pcntl_async_signals(true); 1333 | $this->log('Spawned new helper process with pid ' . $pid, self::LOG_LEVEL_INFO); 1334 | 1335 | // close our socket (we only need the one to the child) 1336 | socket_close($socket_parent); 1337 | 1338 | // track the child 1339 | $this->forked_children[$pid] = array( 1340 | 'ctime' => time(), 1341 | 'identifier' => $identifier, 1342 | 'status' => self::HELPER, 1343 | 'bucket' => self::DEFAULT_BUCKET, 1344 | 'respawn' => $respawn, 1345 | 'function' => $function_name, 1346 | 'arguments' => $arguments, 1347 | 'socket' => $socket_child, 1348 | 'last_active' => microtime(true), 1349 | ); 1350 | $this->forked_children_count++; 1351 | } 1352 | } 1353 | else 1354 | { 1355 | $this->log("Unable to spawn undefined helper function '" . $function_name . "'", self::LOG_LEVEL_CRIT); 1356 | } 1357 | } 1358 | 1359 | /** 1360 | * Forces a helper process to respawn 1361 | * 1362 | * @param string $identifier Id of the helper process to respawn. 1363 | * @return bool|void 1364 | */ 1365 | public function helper_process_respawn($identifier) 1366 | { 1367 | if ($identifier == '') 1368 | { 1369 | return false; 1370 | } 1371 | 1372 | foreach ($this->forked_children as $pid => $child) 1373 | { 1374 | if ($child['status'] == self::HELPER && $child['identifier'] == $identifier) 1375 | { 1376 | $this->safe_kill($pid, SIGKILL, 'Forcing helper process \'' . $identifier . '\' with pid ' . $pid . ' to respawn', self::LOG_LEVEL_INFO); 1377 | } 1378 | } 1379 | } 1380 | 1381 | /** 1382 | * Kill a specified child(ren) by pid 1383 | * 1384 | * Note: This method will block until all requested pids have exited 1385 | * 1386 | * @param int $pids The child pid to kill. 1387 | * @param int $kill_delay How many seconds to wait before sending sig kill on stuck processes. 1388 | * @param int $signal The signal to use to alert the child. 1389 | * @return void 1390 | * @access public 1391 | */ 1392 | public function kill_child_pid($pids, $kill_delay = 30, $signal = SIGINT) 1393 | { 1394 | if (!is_array($pids)) 1395 | { 1396 | $pids = array($pids); 1397 | } 1398 | 1399 | if ($signal == SIGINT) 1400 | { 1401 | $signame = 'sigint'; 1402 | } 1403 | else 1404 | { 1405 | $signame = 'signal ' . $signal; 1406 | } 1407 | 1408 | // send int sigs to the children 1409 | foreach ($pids as $index => $pid) 1410 | { 1411 | // make sure we own this pid 1412 | if (!array_key_exists($pid, $this->forked_children) || $this->forked_children[$pid]['status'] == self::STOPPED) 1413 | { 1414 | $this->log('Skipping kill request on pid ' . $pid . ' because we dont own it', self::LOG_LEVEL_INFO); 1415 | unset($pids[$index]); 1416 | continue; 1417 | } 1418 | 1419 | $this->safe_kill($pid, $signal, 'Asking pid ' . $pid . ' to exit via ' . $signame, self::LOG_LEVEL_INFO); 1420 | } 1421 | 1422 | // store the requst time 1423 | $request_time = microtime(true); 1424 | $time = 0; 1425 | 1426 | // make sure the children exit 1427 | while ((count($pids) > 0) && ($time < $kill_delay)) 1428 | { 1429 | foreach ($pids as $index => $pid) 1430 | { 1431 | // check if the pid exited gracefully 1432 | if (!array_key_exists($pid, $this->forked_children) || $this->forked_children[$pid]['status'] == self::STOPPED) 1433 | { 1434 | $this->log('Pid ' . $pid . ' has exited gracefully', self::LOG_LEVEL_INFO); 1435 | unset($pids[$index]); 1436 | continue; 1437 | } 1438 | 1439 | $time = microtime(true) - $request_time; 1440 | if ($time < $kill_delay) 1441 | { 1442 | $this->log('Waiting ' . ($kill_delay - round($time, 0)) . ' seconds for ' . count($pids) . ' to exit gracefully', self::LOG_LEVEL_INFO); 1443 | sleep(1); 1444 | continue; 1445 | } 1446 | 1447 | $this->safe_kill($pid, SIGKILL, 'Force killing pid ' . $pid, self::LOG_LEVEL_INFO); 1448 | } 1449 | } 1450 | } 1451 | 1452 | /** 1453 | * Process work on the work queue 1454 | * 1455 | * This function will take work sets and hand them off to children. 1456 | * Part of the process is calling invoking fork_work_unit to fork 1457 | * off the child. If $blocking is set to true, this function will 1458 | * process all work units and wait until the children are done until 1459 | * returning. If $blocking is set to false, this function will 1460 | * start as many work units as max_children allows and then return. 1461 | * 1462 | * Note, if $blocking is turned off, the caller has to handle when 1463 | * the children are done with their current load. 1464 | * 1465 | * @param bool $blocking True for blocking mode, false for immediate return. 1466 | * @param int $bucket The bucket to use. 1467 | * @param bool $process_all_buckets If we should process all the buckets. 1468 | * @return bool 1469 | */ 1470 | public function process_work($blocking = true, $bucket = self::DEFAULT_BUCKET, $process_all_buckets = false) 1471 | { 1472 | $this->housekeeping_check(); 1473 | 1474 | // process work on all buckets if desired 1475 | if ($process_all_buckets === true) 1476 | { 1477 | foreach ($this->buckets as $bucket_slot) 1478 | { 1479 | $this->process_work($blocking, $bucket_slot, false); 1480 | } 1481 | return true; 1482 | } 1483 | 1484 | // if room fork children 1485 | if ($blocking === true) 1486 | { 1487 | // process work until completed 1488 | while ($this->work_sets_count($bucket) > 0) 1489 | { 1490 | // check to make sure we have not hit or exceded the max children (globally or within the bucket) 1491 | while ( $this->children_running($bucket) >= $this->max_children[$bucket] ) 1492 | { 1493 | $this->housekeeping_check(); 1494 | $this->signal_handler_sigchild(SIGCHLD); 1495 | sleep(1); 1496 | } 1497 | 1498 | $this->process_work_unit($bucket); 1499 | } 1500 | 1501 | // wait until work finishes 1502 | while ($this->children_running($bucket) > 0) 1503 | { 1504 | sleep(1); 1505 | $this->housekeeping_check(); 1506 | $this->signal_handler_sigchild(SIGCHLD); 1507 | } 1508 | 1509 | // make call back to parent exit function if it exists 1510 | $this->invoke_callback($this->parent_function_exit, array(self::$parent_pid), true); 1511 | } 1512 | else 1513 | { 1514 | // fork children until max 1515 | while ( $this->children_running($bucket) < $this->max_children[$bucket] ) 1516 | { 1517 | if (!$this->child_persistent_mode[$bucket] && $this->work_sets_count($bucket) == 0) 1518 | { 1519 | return true; 1520 | } 1521 | 1522 | $this->process_work_unit($bucket); 1523 | } 1524 | } 1525 | 1526 | return true; 1527 | } 1528 | 1529 | /** 1530 | * Returns the first result available from the bucket. This will run 1531 | * a non-blocking poll of the children for updated results. 1532 | * 1533 | * @param string $bucket The bucket to check. 1534 | * @return mixed The data retrieved from a child process on the buckets 1535 | */ 1536 | public function get_result($bucket = self::DEFAULT_BUCKET) 1537 | { 1538 | // check for additional results 1539 | $this->post_results($bucket); 1540 | 1541 | if (!$this->has_result($bucket)) 1542 | { 1543 | return null; 1544 | } 1545 | 1546 | return array_shift($this->results[$bucket]); 1547 | } 1548 | 1549 | /** 1550 | * Returns all the results currently in the results queue. This will 1551 | * run a non-blocking poll of the children for updated results. 1552 | * 1553 | * @param string $bucket The bucket to retrieves results. 1554 | * @return mixed Array of results from each child that has finished. 1555 | */ 1556 | public function get_all_results($bucket = self::DEFAULT_BUCKET) 1557 | { 1558 | // check for additional results 1559 | $this->post_results($bucket); 1560 | 1561 | if (!$this->has_result($bucket)) 1562 | { 1563 | return array(); 1564 | } 1565 | 1566 | $results = $this->results[$bucket]; 1567 | $this->results[$bucket] = array(); 1568 | 1569 | return $results; 1570 | } 1571 | 1572 | /** 1573 | * Checks if there is a result on the bucket. Before checking, 1574 | * runs a non-blocking poll of the children for updated results. 1575 | * 1576 | * @param string $bucket The bucket to check. 1577 | * @return int Returns true if there is a result 1578 | */ 1579 | public function has_result($bucket = self::DEFAULT_BUCKET) 1580 | { 1581 | // check for additional results 1582 | $this->post_results($bucket); 1583 | 1584 | return (!empty($this->results[$bucket])); 1585 | } 1586 | 1587 | /** 1588 | * Checks if any changed child sockets are in the bucket. 1589 | * 1590 | * @param int $bucket The bucket to get results in. 1591 | * @param int $timeout The timeout for changed socket checking (default 0). 1592 | * @return int|void Returns the number of changed sockets for children workers in $bucket, 1593 | * or empty array if none. 1594 | */ 1595 | protected function get_changed_sockets($bucket = self::DEFAULT_BUCKET, $timeout = 0) 1596 | { 1597 | $write_dummy = null; 1598 | $exception_dummy = null; 1599 | 1600 | // grab all the children sockets 1601 | $sockets = array(); 1602 | foreach ($this->forked_children as $pid => $child) 1603 | { 1604 | if ($child['bucket'] == $bucket) 1605 | { 1606 | $sockets[$pid] = $child['socket']; 1607 | } 1608 | } 1609 | 1610 | if (!empty($sockets)) 1611 | { 1612 | // find changed sockets and return the array of them 1613 | $result = @socket_select($sockets, $write_dummy, $exception_dummy, $timeout); 1614 | if ($result !== false && $result > 0) 1615 | { 1616 | return $sockets; 1617 | } 1618 | } 1619 | 1620 | return null; 1621 | } 1622 | 1623 | /** 1624 | * Returns any pending results from the child sockets. If a 1625 | * child has no results and it has status self::STOPPED, this will remove 1626 | * the child record from $this->forked_children. 1627 | * 1628 | * NOTE: This must be polled to check for changed sockets. 1629 | * 1630 | * @param bool $blocking Set to true to block until a result comes in. 1631 | * @param int $timeout The timeout for changed socket checking (default 0).. 1632 | * @param int $bucket The bucket to look in. 1633 | * @return type The result of the child worker 1634 | */ 1635 | protected function fetch_results($blocking = true, $timeout = 0, $bucket = self::DEFAULT_BUCKET) 1636 | { 1637 | $start = microtime(true); 1638 | $results = array(); 1639 | 1640 | // loop while there is pending children and pending sockets; this 1641 | // will break early on timeouts and when not blocking. 1642 | do 1643 | { 1644 | $ready_sockets = $this->get_changed_sockets($bucket, $timeout); 1645 | if (is_array($ready_sockets)) 1646 | { 1647 | foreach ($ready_sockets as $pid => $socket) 1648 | { 1649 | // Ensure PID is still on forked_children -- may have been removed if a SIGCHILD occurred. The hope 1650 | // is that this fixes BNBS-23987. 1651 | if (!isset($this->forked_children[$pid])) 1652 | { 1653 | unset($ready_sockets[$pid]); 1654 | continue; 1655 | } 1656 | 1657 | $result = $this->socket_receive($socket); 1658 | if ($result !== false && (!is_null($result))) 1659 | { 1660 | $this->forked_children[$pid]['last_active'] = $start; 1661 | $results[$pid] = $result; 1662 | } 1663 | } 1664 | } 1665 | 1666 | // clean up forked children that have stopped and did not have recently 1667 | // active sockets. 1668 | foreach ($this->forked_children as $pid => &$child) 1669 | { 1670 | if (isset($child['last_active']) && ($child['last_active'] < $start) && ($child['status'] == self::STOPPED)) 1671 | { 1672 | // close the socket from the parent 1673 | unset($this->forked_children[$pid]); 1674 | } 1675 | } 1676 | unset($child); 1677 | 1678 | // check if timed out 1679 | if ($timeout && (microtime(true) - $start > $timeout)) 1680 | { 1681 | return $results; 1682 | } 1683 | 1684 | // return null if not blocking and we haven't seen results 1685 | if (!$blocking) 1686 | { 1687 | return $results; 1688 | } 1689 | } while (count($this->forked_children) > 0); 1690 | 1691 | return $results; 1692 | } 1693 | 1694 | /** 1695 | * Posts any new results to a callback function if one is available, or stores 1696 | * them to the internal results storage if not. This does not block and will 1697 | * post any results that are available, so call while children are running 1698 | * to check and post more results. 1699 | * 1700 | * NOTE: This should be polled to update results. 1701 | * 1702 | * @param int $bucket The bucket to post the results in. 1703 | * @return bool Returns true on successfully posting results, even if none 1704 | * to post. Returns false on error from this function or error from 1705 | * the $this->parent_function_results callback. 1706 | */ 1707 | protected function post_results($bucket = self::DEFAULT_BUCKET) 1708 | { 1709 | // fetch all the results up to this point 1710 | $results = $this->fetch_results(false, 0, $bucket); 1711 | if (is_array($results) && empty($results)) 1712 | { 1713 | return true; 1714 | } 1715 | 1716 | if (!empty($this->parent_function_results[$bucket])) 1717 | { 1718 | if ($this->invoke_callback($this->parent_function_results[$bucket], array($results), true) === false) 1719 | { 1720 | return false; 1721 | } 1722 | } 1723 | elseif ($this->store_result === true) 1724 | { 1725 | $this->results[$bucket] += $results; 1726 | } 1727 | 1728 | return true; 1729 | } 1730 | 1731 | /** 1732 | * Pulls items off the work queue for processing 1733 | * 1734 | * Process the work queue by taking up to max_work_per_child items 1735 | * off the queue. A new child is then spawned off to process the 1736 | * work. 1737 | * 1738 | * @param int $bucket The bucket to use. 1739 | * @return void 1740 | */ 1741 | protected function process_work_unit($bucket = self::DEFAULT_BUCKET) 1742 | { 1743 | $child_work_units = array_splice($this->work_units[$bucket], 0, $this->max_work_per_child[$bucket]); 1744 | 1745 | if ($this->child_persistent_mode[$bucket]) 1746 | { 1747 | $data = isset($this->child_persistent_mode_data[$bucket]) ? $this->child_persistent_mode_data[$bucket] : null; 1748 | $this->fork_work_unit($data, '', $bucket); 1749 | } 1750 | elseif (count($child_work_units) > 0) 1751 | { 1752 | if ($this->child_single_work_item[$bucket]) 1753 | { 1754 | // break out identifier and unit 1755 | $child_identifier = key($child_work_units); 1756 | $child_work_unit = current($child_work_units); 1757 | next($child_work_units); 1758 | 1759 | // strip preceeding 'id-' from the identifier 1760 | if (strpos($child_identifier, 'id-') === 0) 1761 | { 1762 | $child_identifier = substr($child_identifier, 3); 1763 | } 1764 | 1765 | // process work unit 1766 | $this->fork_work_unit(array($child_work_unit, $child_identifier), $child_identifier, $bucket); 1767 | } 1768 | else 1769 | { 1770 | $this->fork_work_unit(array($child_work_units), '', $bucket); 1771 | } 1772 | } 1773 | 1774 | // Poll for results from children 1775 | $this->post_results($bucket); 1776 | } 1777 | 1778 | /** 1779 | * Fork one child with one unit of work 1780 | * 1781 | * Given a work unit array, fork a child and hand 1782 | * off the work unit to the child. 1783 | * 1784 | * @param array $work_unit An array of work to process. 1785 | * @param string $identifier A unique identifier for this work. 1786 | * @param int $bucket The bucket to use. 1787 | * @return mixed the child pid on success or boolean false on failure 1788 | */ 1789 | protected function fork_work_unit(array $work_unit, $identifier = '', $bucket = self::DEFAULT_BUCKET) 1790 | { 1791 | // prefork callback 1792 | foreach ($this->parent_function_prefork as $function) 1793 | { 1794 | $this->invoke_callback($function, array(), true); 1795 | } 1796 | 1797 | // init the IPC sockets 1798 | list($socket_child, $socket_parent) = $this->ipc_init(); 1799 | 1800 | // turn off signals temporarily to prevent a SIGCHLD from interupting the parent before $this->forked_children is updated 1801 | pcntl_async_signals(false); 1802 | 1803 | // spoon! 1804 | $pid = pcntl_fork(); 1805 | 1806 | if ($pid == -1) 1807 | { 1808 | /** 1809 | * Fork Error 1810 | */ 1811 | 1812 | $this->log('failed to fork', self::LOG_LEVEL_CRIT); 1813 | return false; 1814 | } 1815 | elseif ($pid) 1816 | { 1817 | /** 1818 | * Parent Process 1819 | */ 1820 | 1821 | // keep track of this pid in the parent 1822 | $this->forked_children[$pid] = array( 1823 | 'ctime' => time(), 1824 | 'identifier' => $identifier, 1825 | 'bucket' => $bucket, 1826 | 'status' => self::WORKER, 1827 | 'socket' => $socket_child, 1828 | 'last_active' => microtime(true), 1829 | ); 1830 | $this->forked_children_count++; 1831 | 1832 | // turn back on signals now that $this->forked_children has been updated 1833 | pcntl_async_signals(true); 1834 | 1835 | // close our socket (we only need the one to the child) 1836 | socket_close($socket_parent); 1837 | 1838 | // debug logging 1839 | $this->log('forking child ' . $pid . ' for bucket ' . $bucket, self::LOG_LEVEL_DEBUG); 1840 | 1841 | // parent spawned child callback 1842 | $this->invoke_callback($this->parent_function_fork[$bucket], array($pid, $identifier), true); 1843 | } 1844 | else 1845 | { 1846 | /** 1847 | * Child Process 1848 | */ 1849 | 1850 | if($this->free_unused_memory_in_child) 1851 | { 1852 | // free up unneeded parent memory for child process 1853 | $this->work_units = null; 1854 | $this->forked_children = null; 1855 | $this->results = null; 1856 | } 1857 | 1858 | // set child properties 1859 | $this->child_bucket = $bucket; 1860 | 1861 | // turn signals on for the child 1862 | pcntl_async_signals(true); 1863 | 1864 | // close our socket (we only need the one to the parent) 1865 | socket_close($socket_child); 1866 | 1867 | // re-seed the random generator to prevent clone from parent 1868 | srand(); 1869 | mt_srand(); 1870 | 1871 | // child run callback 1872 | $result = $this->invoke_callback($this->child_function_run[$bucket], $work_unit, false); 1873 | 1874 | // send the result to the parent 1875 | self::socket_send($socket_parent, $result); 1876 | 1877 | // delay the child's exit slightly to avoid race conditions 1878 | usleep(500); 1879 | 1880 | // exit after we complete one unit of work 1881 | exit; 1882 | } 1883 | 1884 | return $pid; 1885 | } 1886 | 1887 | /** 1888 | * Performs house keeping every housekeeping_check_interval seconds 1889 | * @access protected 1890 | * @return void 1891 | */ 1892 | protected function housekeeping_check() 1893 | { 1894 | if ((time() - $this->housekeeping_last_check) >= $this->housekeeping_check_interval) 1895 | { 1896 | // check to make sure no children are violating the max run time 1897 | $this->kill_maxtime_violators(); 1898 | 1899 | // look for zombie children just in case 1900 | $this->signal_handler_sigchild(SIGCHLD); 1901 | 1902 | // update the last check time to now 1903 | $this->housekeeping_last_check = time(); 1904 | } 1905 | } 1906 | 1907 | /** 1908 | * Kills any children that have been running for too long. 1909 | * @access protected 1910 | * @return void 1911 | */ 1912 | protected function kill_maxtime_violators() 1913 | { 1914 | foreach ($this->forked_children as $pid => $pid_info) 1915 | { 1916 | if ($pid_info['status'] == self::STOPPED) 1917 | { 1918 | continue; 1919 | } 1920 | 1921 | if ($this->child_max_run_time[$pid_info['bucket']] >= 0 && (time() - $pid_info['ctime']) > $this->child_max_run_time[$pid_info['bucket']]) 1922 | { 1923 | $this->log('Force kill ' . $pid . ' has run too long', self::LOG_LEVEL_INFO); 1924 | 1925 | // notify app that child process timed out 1926 | $this->invoke_callback($this->child_function_timeout{$pid_info['bucket']}, array($pid, $pid_info['identifier']), true); 1927 | 1928 | $this->safe_kill($pid, SIGKILL); // its probably stuck on something, kill it immediately. 1929 | sleep(3); // give the child time to die 1930 | 1931 | // force signal handling 1932 | $this->signal_handler_sigchild(SIGCHLD); 1933 | } 1934 | } 1935 | } 1936 | 1937 | /** 1938 | * Invoke a call back function with parameters 1939 | * 1940 | * Given the name of a function and parameters to send it, invoke the function. 1941 | * This function will try using the objects inherited function if it exists. If not, 1942 | * it'll look for a declared function of the given name. 1943 | * 1944 | * @access protected 1945 | * @param string $function_name The name of the function to invoke. 1946 | * @param array $parameters An array of parameters to pass to function. 1947 | * @param bool $optional Is set to true, don't error if function_name not available. 1948 | * @return mixed false on error, otherwise return of callback function 1949 | */ 1950 | protected function invoke_callback($function_name, array $parameters, $optional = false) 1951 | { 1952 | // call child function 1953 | if (is_callable($function_name)) 1954 | { 1955 | if (!is_array($parameters)) 1956 | { 1957 | $parameters = array($parameters); 1958 | } 1959 | 1960 | return call_user_func_array($function_name, $parameters); 1961 | } 1962 | elseif (is_array($function_name) && method_exists($function_name[0], $function_name[1])) 1963 | { 1964 | if (!is_array($parameters)) 1965 | { 1966 | $parameters = array($parameters); 1967 | } 1968 | 1969 | return call_user_func_array($function_name, $parameters); 1970 | } 1971 | elseif (method_exists($this, $function_name) ) 1972 | { 1973 | if (!is_array($parameters)) 1974 | { 1975 | $parameters = array($parameters); 1976 | } 1977 | 1978 | return call_user_func_array($this->$function_name, $parameters); 1979 | } 1980 | else if (function_exists($function_name)) 1981 | { 1982 | if (!is_array($parameters)) 1983 | { 1984 | $parameters = array($parameters); 1985 | } 1986 | 1987 | return call_user_func_array($function_name, $parameters); 1988 | } 1989 | else 1990 | { 1991 | if ($optional === false) 1992 | { 1993 | $this->log("Error there are no functions declared in scope to handle callback for function '" . $function_name . "'", self::LOG_LEVEL_CRIT); 1994 | } 1995 | } 1996 | } 1997 | 1998 | /** 1999 | * Initialize interprocess communication by setting up a pair 2000 | * of sockets and returning them as an array. 2001 | * 2002 | * @return type 2003 | */ 2004 | protected function ipc_init() 2005 | { 2006 | // windows needs AF_INET 2007 | $domain = strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' ? AF_INET : AF_UNIX; 2008 | 2009 | // create a socket pair for IPC 2010 | $sockets = array(); 2011 | if (socket_create_pair($domain, SOCK_STREAM, 0, $sockets) === false) 2012 | { 2013 | $this->log('socket_create_pair failed: ' . socket_strerror(socket_last_error()), self::LOG_LEVEL_CRIT); 2014 | return false; 2015 | } 2016 | 2017 | // return the sockets 2018 | return $sockets; 2019 | } 2020 | 2021 | /** 2022 | * Sends a serializable message to the socket. 2023 | * 2024 | * @param resource $socket The socket to send the message on. 2025 | * @param mixed $message The serializable message to send. 2026 | * @return type Returns true on success, false on failure 2027 | */ 2028 | protected function socket_send($socket, $message) 2029 | { 2030 | $serialized_message = @serialize($message); 2031 | if ($serialized_message == false) 2032 | { 2033 | $this->log('socket_send failed to serialize message', self::LOG_LEVEL_CRIT); 2034 | return false; 2035 | } 2036 | 2037 | $header = pack('N', strlen($serialized_message)); 2038 | $data = $header . $serialized_message; 2039 | $bytes_left = strlen($data); 2040 | while ($bytes_left > 0) 2041 | { 2042 | $bytes_sent = @socket_write($socket, $data); 2043 | if ($bytes_sent === false) 2044 | { 2045 | $this->log('socket_send error: ' . socket_strerror(socket_last_error()), self::LOG_LEVEL_CRIT); 2046 | return false; 2047 | } 2048 | 2049 | $bytes_left -= $bytes_sent; 2050 | $data = substr($data, $bytes_sent); 2051 | } 2052 | 2053 | return true; 2054 | } 2055 | 2056 | /** 2057 | * Receives a serialized message from the socket. 2058 | * 2059 | * @param resource $socket The socket to receive the message from. 2060 | * @return mixed Returns data on success, false on failure 2061 | */ 2062 | protected function socket_receive($socket) 2063 | { 2064 | // initially read to the length of the header size, then 2065 | // expand to read more 2066 | $bytes_total = self::SOCKET_HEADER_SIZE; 2067 | $bytes_read = 0; 2068 | $have_header = false; 2069 | $socket_message = ''; 2070 | while ($bytes_read < $bytes_total) 2071 | { 2072 | $read = @socket_read($socket, $bytes_total - $bytes_read); 2073 | if ($read === false) 2074 | { 2075 | $this->log('socket_receive error: ' . socket_strerror(socket_last_error()), self::LOG_LEVEL_CRIT); 2076 | return false; 2077 | } 2078 | 2079 | // blank socket_read means done 2080 | if ($read == '') 2081 | { 2082 | break; 2083 | } 2084 | 2085 | $bytes_read += strlen($read); 2086 | $socket_message .= $read; 2087 | 2088 | if (!$have_header && $bytes_read >= self::SOCKET_HEADER_SIZE) 2089 | { 2090 | $have_header = true; 2091 | list($bytes_total) = array_values(unpack('N', $socket_message)); 2092 | $bytes_read = 0; 2093 | $socket_message = ''; 2094 | } 2095 | } 2096 | 2097 | $message = @unserialize($socket_message); 2098 | 2099 | return $message; 2100 | } 2101 | 2102 | /** 2103 | * Log a message 2104 | * 2105 | * @access protected 2106 | * @param string $message The text to log. 2107 | * @param int $severity The severity of the message. 2108 | * @return bool true on success, false on error 2109 | */ 2110 | protected function log($message, $severity) 2111 | { 2112 | if (!empty($this->log_function)) 2113 | { 2114 | if (isset($this->log_function[$severity])) 2115 | { 2116 | return call_user_func_array($this->log_function[$severity], array($message, $severity)); 2117 | } 2118 | elseif (isset($this->log_function[self::LOG_LEVEL_ALL])) 2119 | { 2120 | return call_user_func_array($this->log_function[self::LOG_LEVEL_ALL], array($message, $severity)); 2121 | } 2122 | } 2123 | // Barracuda specific logging class, to keep internal code working 2124 | elseif (method_exists('Log', 'message')) 2125 | { 2126 | return Log::message($message, $severity); 2127 | } 2128 | 2129 | return true; 2130 | } 2131 | } 2132 | --------------------------------------------------------------------------------