├── .gitignore ├── src └── Kachkaev │ └── PHPR │ ├── Exception │ ├── RException.php │ ├── RProcessException.php │ ├── IncompleteRCommandException.php │ └── RErrorsException.php │ ├── Engine │ ├── CommandLineREngine.php │ ├── ServerBasedREngine.php.stub │ ├── REngineInterface.php │ └── AbstractREngine.php │ ├── RCore.php │ ├── RError.php │ ├── ROutputParser.php │ └── Process │ ├── CommandLineRProcess.php │ ├── RProcessInterface.php │ └── AbstractRProcess.php ├── composer.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | composer.phar 3 | vendor/* 4 | -------------------------------------------------------------------------------- /src/Kachkaev/PHPR/Exception/RException.php: -------------------------------------------------------------------------------- 1 | " 8 | */ 9 | class RException extends \Exception 10 | { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/Kachkaev/PHPR/Exception/RProcessException.php: -------------------------------------------------------------------------------- 1 | " 10 | */ 11 | class RProcessException extends RException 12 | { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/Kachkaev/PHPR/Engine/CommandLineREngine.php: -------------------------------------------------------------------------------- 1 | rCommand = $rCommand; 13 | } 14 | 15 | protected function createProcess() 16 | { 17 | return new CommandLineRProcess($this->rCommand); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Kachkaev/PHPR/Engine/ServerBasedREngine.php.stub: -------------------------------------------------------------------------------- 1 | serverURL = $serverURL; 13 | } 14 | 15 | protected function createProcess() 16 | { 17 | throw new \RuntimeException('Engine not yet implemented'); 18 | 19 | return new ServerBasedRProcess($this->serverURL); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Kachkaev/PHPR/RCore.php: -------------------------------------------------------------------------------- 1 | rEngine = $rEngine; 14 | } 15 | 16 | public function run($rCode, $resultAsArray = false, $isErrorSensitive = false) 17 | { 18 | return $this->rEngine->run($rCode, $resultAsArray, $isErrorSensitive); 19 | } 20 | 21 | public function createInteractiveProcess($isErrorSensitive = false) 22 | { 23 | return $this->rEngine->createInteractiveProcess($isErrorSensitive); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Kachkaev/PHPR/Engine/REngineInterface.php: -------------------------------------------------------------------------------- 1 | write(?) with input resulting 14 | * errors causes the process to throw RErrosException. 15 | * Otherwise they are accessible via $rProcess->getLastWriteErrors() 16 | * The option can always be changed using $rProcess->setErrorSensitive(true/false) 17 | * 18 | * @return RProcessInterface 19 | */ 20 | public function createInteractiveProcess($isErrorSensitive = false); 21 | } 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kachkaev/php-r", 3 | "type": "library", 4 | "description": "Provides ability to run R scripts from PHP", 5 | "keywords": ["r", "statistical analysis", "stats", "bridge"], 6 | "homepage": "https://github.com/kachkaev/php-r", 7 | "license": "MIT", 8 | "authors": [{ 9 | "name": "Alexander Kachkaev", 10 | "email": "alexander@kachkaev.ru", 11 | "homepage": "http://en.kachkaev.ru/" 12 | }, 13 | { 14 | "name": "Contributors", 15 | "homepage": "https://github.com/kachkaev/php-r/contributors" 16 | }], 17 | "require": { 18 | "php": ">=5.3.9" 19 | }, 20 | "autoload": { 21 | "psr-0": { 22 | "Kachkaev\\PHPR\\": "src" 23 | } 24 | }, 25 | "extra": { 26 | "branch-alias": { 27 | "dev-master": "1.0.x-dev" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Kachkaev/PHPR/Exception/IncompleteRCommandException.php: -------------------------------------------------------------------------------- 1 | " 10 | */ 11 | class IncompleteRCommandException extends RProcessException 12 | { 13 | public function __construct($command) 14 | { 15 | if (!is_string($command)) { 16 | throw new \InvalidArgumentException( 17 | 'Argument $command in constructor of IncompleteRCommandException must be a string'); 18 | } 19 | 20 | $this->command = $command; 21 | 22 | $message = 'The last command in the R input is not complete (missing closing bracket, quotation mark, etc.)'; 23 | parent::__construct($message, 0, null); 24 | 25 | } 26 | 27 | public function getCommand() 28 | { 29 | return $this->command; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Alexander Kachkaev 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | 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, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /src/Kachkaev/PHPR/RError.php: -------------------------------------------------------------------------------- 1 | " 9 | */ 10 | class RError 11 | { 12 | private $inputLineNumber; 13 | private $commandNumber; 14 | private $command; 15 | private $errorMessage; 16 | 17 | public function __construct($inputLineNumber, $commandNumber, $command, $errorMessage) 18 | { 19 | $this->inputLineNumber = $inputLineNumber; 20 | $this->commandNumber = $commandNumber; 21 | $this->command = $command; 22 | $this->errorMessage = $errorMessage; 23 | } 24 | 25 | /** 26 | * @return int zero-based R command line number that caused the error 27 | */ 28 | public function getInputLineNumber() 29 | { 30 | return $this->inputLineNumber; 31 | } 32 | 33 | /** 34 | * @return int zero-based R command number that caused the error 35 | */ 36 | public function getCommandNumber() 37 | { 38 | return $this->commandNumber; 39 | } 40 | 41 | /** 42 | * @return string returns the R command the caused the error 43 | */ 44 | public function getCommand() 45 | { 46 | return $this->command; 47 | } 48 | 49 | /** 50 | * @return string message generated by R 51 | */ 52 | public function getErrorMessage() 53 | { 54 | return $this->errorMessage; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Kachkaev/PHPR/ROutputParser.php: -------------------------------------------------------------------------------- 1 | " 9 | * 10 | */ 11 | class ROutputParser 12 | { 13 | /** 14 | * "[1] 42" 15 | * ↓ 16 | * 42 17 | * 18 | * @param string $output 19 | * 20 | * @return numeric 21 | */ 22 | public function singleNumber($output) 23 | { 24 | //TODO properly check if r output is a valid number 25 | return 0 + substr($output, 4); 26 | } 27 | 28 | /** 29 | * " [1] 100 200 300 ... 1200" 30 | * "[13] 1300 1400 1500 ..." 31 | * ↓ 32 | * [100, 200, 300 ...] 33 | * 34 | * @param string $output 35 | * 36 | * @return array 37 | */ 38 | public function numericVector($output) 39 | { 40 | //TODO properly check if r output is a valid vector 41 | $result = array(); 42 | 43 | foreach (explode("\n", $output) as $row) { 44 | // Cut off [?] if needed 45 | If (strpos($row, ']') !== false) { 46 | $numbersAsStr = substr($row, strpos($row, ']') + 1); 47 | } else { 48 | $numbersAsStr = $row; 49 | } 50 | foreach (explode(' ', $numbersAsStr) as $potentialNumber) { 51 | if ($potentialNumber !== '') { 52 | array_push($result, 0 + $potentialNumber); 53 | } 54 | } 55 | } 56 | 57 | return $result; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Kachkaev/PHPR/Engine/AbstractREngine.php: -------------------------------------------------------------------------------- 1 | createInteractiveProcess($isErrorSensitive); 23 | $rProcess->start(); 24 | try { 25 | $rProcess->write($rCode); 26 | $errorsException = null; 27 | } catch (RErrorsException $e) { 28 | $errorsException = $e; 29 | } 30 | $rProcess->stop(); 31 | 32 | $result = $rProcess->getAllResult($resultAsArray); 33 | unset($rProcess); 34 | 35 | if ($errorsException) { 36 | throw $errorsException; 37 | } 38 | 39 | return $result; 40 | } 41 | 42 | /** 43 | * (non-PHPdoc) 44 | * @see \Kachkaev\PHPR\REngine\REngineInterface::createInteractiveProcess() 45 | */ 46 | public function createInteractiveProcess($isErrorSensitive = false) 47 | { 48 | $rProcess = $this->createProcess(); 49 | if ($isErrorSensitive) { 50 | $rProcess->setErrorSensitive($isErrorSensitive); 51 | } 52 | 53 | return $rProcess; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Kachkaev/PHPR/Exception/RErrorsException.php: -------------------------------------------------------------------------------- 1 | " 12 | */ 13 | class RErrorsException extends RException 14 | { 15 | private $inputLog; 16 | private $outputLog; 17 | private $errors; 18 | 19 | public function __construct(array $inputLog, array $outputLog, 20 | array $errors) 21 | { 22 | foreach ($inputLog as $inputLogElement) { 23 | if (!is_string($inputLogElement)) { 24 | throw new \InvalidArgumentException( 25 | 'Argument $inputLog in constructor of RErrorsException must be an array of strings'); 26 | } 27 | } 28 | 29 | foreach ($outputLog as $outputLogElement) { 30 | if (!is_null($outputLogElement) && !is_string($outputLogElement)) { 31 | throw new \InvalidArgumentException( 32 | 'Argument $outputLog in constructor of RErrorsException must be an array of strings or nulls'); 33 | } 34 | } 35 | if (!count($errors)) { 36 | throw new \InvalidArgumentException( 37 | 'Argument $errors in constructor of RErrorsException must contain at least one error'); 38 | } 39 | 40 | foreach ($errors as $error) { 41 | if (!($error instanceof RError)) { 42 | throw new \InvalidArgumentException( 43 | 'Argument $errors in constructor of RErrorsException must be an array of RError'); 44 | } 45 | } 46 | 47 | $this->inputLog = $inputLog; 48 | $this->outputLog = $outputLog; 49 | $this->errors = $errors; 50 | 51 | $errorCount = count($errors); 52 | $message = $errorCount == 1 ? 'One error occurred when running a chunk of R script: ' 53 | : sprintf( 54 | '%d errors occurred when running a chunk of R script. First: ', 55 | $errorCount); 56 | 57 | $message .= $errors[0]->getErrorMessage(); 58 | 59 | parent::__construct($message, 0, null); 60 | } 61 | 62 | /** 63 | * @return array 64 | */ 65 | public function getInputLog() 66 | { 67 | return $this->inputLog; 68 | } 69 | 70 | /** 71 | * @return array 72 | */ 73 | public function getOutputLog() 74 | { 75 | return $this->outputLog; 76 | } 77 | 78 | /** 79 | * @return array 80 | */ 81 | public function getErrors() 82 | { 83 | return $this->errors; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Kachkaev/PHPR/Process/CommandLineRProcess.php: -------------------------------------------------------------------------------- 1 | rCommand = $rCommand; 21 | } 22 | 23 | function doStart() 24 | { 25 | $descriptors = array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 26 | 2 => array("pipe", "w"),); 27 | 28 | $this->process = proc_open( 29 | sprintf("\"%s\" --silent --vanilla", $this->rCommand), 30 | $descriptors, 31 | $this->pipes 32 | ); 33 | 34 | if (!is_resource($this->process)) { 35 | throw new RProcessException('Could not create the process'); 36 | } 37 | 38 | stream_set_blocking($this->pipes[2], false); 39 | 40 | $errorOutput = fgets($this->pipes[2]); 41 | if ($errorOutput) { 42 | throw new RProcessException($errorOutput); 43 | } 44 | 45 | // Skip the startup message (if any) 46 | do { 47 | $out = fread($this->pipes[1], $this->infiniteLength); 48 | usleep($this->sleepTimeBetweenReads); 49 | } while ($out !== '> ' && substr($out, -3) !== "\n> "); 50 | 51 | // Do not terminate on errors 52 | fwrite($this->pipes[0], "options(error=expression(NULL))\n"); 53 | fread($this->pipes[1], $this->infiniteLength); 54 | } 55 | 56 | function doStop() 57 | { 58 | fclose($this->pipes[0]); 59 | fclose($this->pipes[1]); 60 | fclose($this->pipes[2]); 61 | proc_close($this->process); 62 | } 63 | 64 | function doWrite(array $rInputLines) 65 | { 66 | $currentCommandInput = ''; 67 | $currentCommandOutput = ''; 68 | $currentCommandErrorOutput = ''; 69 | 70 | foreach ($rInputLines as $rInputLine) { 71 | ++$this->inputLineCount; 72 | 73 | if (empty(trim($rInputLine))) { 74 | continue; 75 | } 76 | 77 | // Write the input into the pipe 78 | fwrite($this->pipes[0], $rInputLine . "\n"); 79 | 80 | // Read back the input 81 | do { 82 | $readLine = fread($this->pipes[1], $this->infiniteLength); 83 | } while (trim($readLine) !== trim($rInputLine)); 84 | $currentCommandInput .= $readLine; 85 | 86 | // Read the output 87 | $commandIsIncomplete = false; 88 | do { 89 | $output = fread($this->pipes[1], $this->infiniteLength); 90 | 91 | // Append the output 92 | $currentCommandOutput .= $output; 93 | $currentCommandErrorOutput .= fread($this->pipes[2], $this->infiniteLength); 94 | 95 | // If the output is "+ ", then it is a multi-line command 96 | if (substr($output, -3) === "\n+ " || $output === '+ ') { 97 | $commandIsIncomplete = true; 98 | $currentCommandOutput = ''; 99 | 100 | // A multi-line command that does not finish is a fatal case 101 | if ($rInputLine === end($rInputLines)) { 102 | throw new IncompleteRCommandException($currentCommandInput); 103 | } 104 | break; 105 | } 106 | 107 | usleep($this->sleepTimeBetweenReads); 108 | } while ($output !== '> ' && substr($output, -3) !== "\n> "); 109 | 110 | // Continue reading input if it is a multi-line command 111 | if ($commandIsIncomplete) { 112 | continue; 113 | } 114 | 115 | // Trim "\n" from the command input 116 | $currentCommandInput = substr($currentCommandInput, 0, -1); 117 | // Trim "\n> " from the command output 118 | $currentCommandOutput = substr($currentCommandOutput, 0, -3); 119 | if ($currentCommandOutput === false) { 120 | $currentCommandOutput = null; 121 | } 122 | // Trim "\n" from the error input 123 | $currentCommandErrorOutput = substr($currentCommandErrorOutput, 0, -1); 124 | 125 | // Add input and output to logs 126 | $this->inputLog[] = $currentCommandInput; 127 | $this->outputLog[] = $currentCommandOutput; 128 | ++$this->lastWriteCommandCount; 129 | 130 | // Register an error if needed 131 | if ($currentCommandErrorOutput) { 132 | $error = new RError($this->inputLineCount - 1, count($this->inputLog) - 1, 133 | $currentCommandInput, $currentCommandErrorOutput); 134 | ++$this->lastWriteErrorCount; 135 | $this->errors[] = $error; 136 | } 137 | 138 | // Reset buffers 139 | $currentCommandInput = ''; 140 | $currentCommandOutput = ''; 141 | $currentCommandErrorOutput = ''; 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | php-r 2 | ===== 3 | 4 | PHPR (or php-r) is a library that provides ability to run R scripts from PHP. Composer package: [kachkaev/php-r](https://packagist.org/packages/kachkaev/php-r). 5 | 6 | Optionally, the library is available [as a bundle](https://github.com/kachkaev/KachkaevPHPRBundle) for Symfony2 users. 7 | 8 | The idea is based on invoking a command-line version of R and exchanging messages with this external process. 9 | Integration with server-based implementation of R can be easily implemented on demand as the library architecture supports that. 10 | 11 | It is possible to both run all R code in one batch and interactively exchange commands with R interpreter. 12 | 13 | Usage 14 | ----- 15 | ### Option 1: all R code in one batch 16 | ```php 17 | use Kachkaev\PHPR\RCore; 18 | use Kachkaev\PHPR\Engine\CommandLineREngine; 19 | 20 | $r = new RCore(new CommandLineREngine('/path/to/R')); 21 | 22 | $result = $r->run(<< x = 1 37 | > y = 2 38 | > x + y 39 | [1] 3 40 | > x + z 41 | Error: object 'z' not found 42 | > x - y 43 | [1] -1 44 | ``` 45 | 46 | Method ```run()``` is always called in a clean scope of R variables, i.e. the following usage will result an R error: 47 | 48 | ```php 49 | echo $r->run("x = 100") 50 | echo "\n=====\n" 51 | echo $r->run("x * x") 52 | ``` 53 | 54 | PHP output: 55 | ``` 56 | > x = 100 57 | ===== 58 | > x * x 59 | Error: object 'x' not found 60 | ``` 61 | 62 | ### Option 2: interactive exchange of data 63 | To exchange commands with a single R process interactively, another approach should be used: 64 | ```php 65 | use Kachkaev\PHPR\RCore; 66 | use Kachkaev\PHPR\Engine\CommandLineREngine; 67 | 68 | $r = new RCore(new CommandLineREngine('/path/to/R')); 69 | $rProcess = $r->createInteractiveProcess(); 70 | $rProcess->start(); 71 | $rProcess->write('x = 100'); 72 | // Do something else 73 | $rProcess->write('x * x'); 74 | 75 | echo $rProcess->getAllResult(); 76 | ``` 77 | 78 | PHP output: 79 | ``` 80 | > x = 100 81 | > x * x 82 | [1] 10000 83 | ``` 84 | 85 | The process is synchronous, i.e. if R code sent to ```write()``` implies some complex computations, PHP will wait until they are finished. 86 | 87 | Multiple commands can be passed to R inside one ```write()```; they can be multi-line too: 88 | 89 | ```php 90 | $rProcess->write(<<write('x = 1 + ('); 103 | // IncompleteRCommandException 104 | ``` 105 | 106 | #### Separate access to input / output / errors for each R command 107 | To avoid manual splitting of a mix of R input, output and errors, the result of script execution can be accessed separately: 108 | ```php 109 | $rProcess = $r->createInteractiveProcess(); 110 | $rProcess->start(); 111 | $rProcess->write(<<write(<<getLastWriteInput(); 127 | // x + 41 128 | // x + xxx 129 | // x + y 130 | 131 | echo $rProcess->getLastWriteOutput(); 132 | // 42 133 | // 134 | // [1] 3 135 | 136 | echo $rProcess->hasLastWriteErrors(); 137 | // true 138 | 139 | echo $rProcess->getLastWriteErrorCount(); 140 | // 1 141 | 142 | $errors = $rProcess->getLastWriteErrors(); 143 | echo $errors[0]->getErrorMessage() 144 | // object 'xxx' not found 145 | echo $errors[0]->getCommand() 146 | // x + xxx 147 | 148 | $rProcess->getAllInput(); 149 | $rProcess->getAllOutput(); 150 | $rProcess->hasErrors(); 151 | $rProcess->getErrorCount(); 152 | $rProcess->getErrors(); 153 | ``` 154 | 155 | Passing ```true``` to ```get(LastWrite|All)Input/get(LastWrite|All)Output/get(LastWrite|All)Result``` splits strings into arrays, where each element corresponds to a single command: 156 | 157 | ```php 158 | $rProcess = $r->createInteractiveProcess(); 159 | $rProcess->start(); 160 | $rProcess->write(<<getAllInput(true); 168 | // ['x ={newline}1 + 1', 'y', 'x'] 169 | $outputAsArray = $rProcess->getAllOutput(true); 170 | // ['', null, '2'] 171 | $resultAsArray = $rProcess->getAllResult(true); 172 | // [ 173 | // ['x ={newline}1 + 1', '', null], 174 | // ['y', null, 'Error: object \'y\' not found'], 175 | // ['x', '2', null] 176 | // ] 177 | ``` 178 | 179 | #### Sensitivity to R errors 180 | If it is necessary to make sure that a sequence of R commands is running with no errors, and calling ```hasLastWriteErrors()``` after each ```write()``` is unreasonable, the process can be made sensible to errors. 181 | ```RErrorsException``` will be thrown on ```write()```: 182 | 183 | ```php 184 | $rProcess->setErrorSensitive(true); 185 | $rProcess->write('x = 1 + missingVariable'); 186 | // RErrorsException 187 | ``` 188 | 189 | This is the same as: 190 | ```php 191 | $rProcess->setErrorSensitive(false); 192 | $rProcess->write('x = 1 + missingVariable'); 193 | if ($rProcess->hasLastWriteErrors()) { 194 | throw new RErrorsException($rProcess->getLastWriteInput(true), $rProcess->getLastWriteOutput(true), $rProcess->getLastWriteErrors()); 195 | } 196 | ``` 197 | 198 | R-related errors and the exception thrown are not critical; the same instance of R process can be still used after they occur. If last input contains multiple commands, and several of them cause errors, ```RErrorsException``` will have the complete list. In any case all commands passed to ```write()``` will be attempted by R interpreter. 199 | 200 | ```php 201 | $allErrors = $rErrorsException->getErrors(); 202 | if (count ($allErrors) > 1) { 203 | $secondError = $allErrors[1]; 204 | echo $secondError->getErrorMessage(); 205 | } 206 | ``` 207 | 208 | #### Parsing R output 209 | 210 | To ease parsing of R output, ```ROutputParser``` can be used: 211 | 212 | ```php 213 | use Kachkaev\PHPR\ROutputParser; 214 | 215 | $rOutputParser = new ROutputParser(); 216 | $rProcess->write('21 + 21'); 217 | var_dump($rProcess->getLastWriteOutput()); 218 | // string(6) "[1] 42" 219 | var_dump($rOutputParser->singleNumber($rProcess->getLastWriteOutput())); 220 | // int(42) 221 | ``` 222 | 223 | See PHPdoc annotations to classes for more details. 224 | 225 | License 226 | ------- 227 | MIT. See [LICENSE](LICENSE). 228 | 229 | [![Donate](https://img.shields.io/badge/donate-paypal-blue.svg?style=flat-rounded)](https://paypal.me/kachkaev/5gbp) 230 | -------------------------------------------------------------------------------- /src/Kachkaev/PHPR/Process/RProcessInterface.php: -------------------------------------------------------------------------------- 1 | " 20 | * 21 | */ 22 | interface RProcessInterface 23 | { 24 | /** 25 | * Starts the R process and also resets errors, input and output 26 | * 27 | * @throws RProcessException if the process is running 28 | */ 29 | public function start(); 30 | 31 | /** 32 | * Stops the R process 33 | * 34 | * @throws RProcessException if the process is not running 35 | */ 36 | public function stop(); 37 | 38 | /** 39 | * Restarts the R process (stops and starts it). 40 | * 41 | * @throws RProcessException if the process is not running 42 | */ 43 | public function restart(); 44 | 45 | /** 46 | * Checks if the R process is running 47 | * @return boolean true if the process has been started, but not stopped; false otherwise 48 | */ 49 | public function isRunning(); 50 | 51 | /** 52 | * Writes lines of commands to R interpreter 53 | * 54 | * @param string $rInput a multi-line string with commands to execute (no trailing EOL symbol is needed) 55 | * @return integer the number of errors during the execution (same as getLastWriteErrorCount()) 56 | * 57 | * @throws RProcessException if the given input does not form a complete 58 | * command (e.g. "1 + ("), which makes R waiting 59 | * for the rest of a multi-line command. 60 | * Such case is fatal; the process stops. 61 | */ 62 | public function write($rInput); 63 | 64 | /** 65 | * Returns all input to the R interpreter 66 | * 67 | * @param boolean $asArray if set to true, an array of strings is returned instead of a single string 68 | * (the input is split by commands) 69 | * @return string|array all input to R 70 | */ 71 | public function getAllInput($asArray = false); 72 | 73 | /** 74 | * Returns all output from the R interpreter 75 | * 76 | * @param boolean $asArray if set to true, an array of strings is returned instead of a single string 77 | * (the output is split by input commands) 78 | * @return string|array all output from R 79 | */ 80 | public function getAllOutput($asArray = false); 81 | 82 | /** 83 | * Returns all input, output and errors (as text or array, depending on $asArray parameter) 84 | * 85 | * As text: 86 | * -------- 87 | * > 2*2 88 | * [1] 4 89 | * > 2*( 90 | * + 1+1) 91 | * [1] 4 92 | * > <- empty lines are not followed by lines with output (no ">") 93 | * > 94 | * > 95 | * > a*2 96 | * Error:object 'a' not found 97 | * 98 | * 99 | * As array: 100 | * --------- 101 | * ['input1', 'output1', null] 102 | * ['input2', null, 'error2'] 103 | * 104 | * 0th element: always a string 105 | * 1st and 2nd elements: a string or null 106 | * 107 | * @param boolean $asArray if set to true, the result is returned as an array 108 | * @return string|array result of R execution 109 | */ 110 | public function getAllResult($asArray = false); 111 | 112 | /** 113 | * Returns the most recent input to the R interpreter (since the last call of write() method) 114 | * 115 | * @param boolean $asArray if set to true, an array of strings is returned instead of a single string 116 | * (the input is split by commands) 117 | * @return string|array all input to R 118 | */ 119 | public function getLastWriteInput($asArray = false); 120 | 121 | /** 122 | * Returns the most recent output from the R interpreter (since the last call of write() method) 123 | * 124 | * @param boolean $asArray if set to true, an array of strings is returned instead of a single string 125 | * (the output is split by input commands) 126 | * @return string|array all output from R 127 | */ 128 | public function getLastWriteOutput($asArray = false); 129 | 130 | /** 131 | * Returns the most recent input, output and errors (since the last call of write() method) 132 | * 133 | * For details on format see getAllResult() 134 | * 135 | * @param boolean $asArray if set to true, the result is returned as an array 136 | * @return string|array result of R execution 137 | */ 138 | public function getLastWriteResult($asArray = false); 139 | 140 | /** 141 | * Determines if there were errors since the last call of start() method 142 | * 143 | * @return boolean true if there were errors since the last call of start() method, false otherwise 144 | */ 145 | public function hasErrors(); 146 | 147 | /** 148 | * Gets the number of errors that occurred since the last call of start() method 149 | * 150 | * @return integer 151 | */ 152 | public function getErrorCount(); 153 | 154 | /** 155 | * Gets the array of errors (elements are of type RError) since the last call of start() method 156 | * 157 | * @return array 158 | */ 159 | public function getErrors(); 160 | 161 | /** 162 | * Determines if there were errors since the last call of write() method 163 | * 164 | * @return boolean true if there were errors since the last call of write() method, false otherwise 165 | */ 166 | public function hasLastWriteErrors(); 167 | 168 | /** 169 | * Gets the number of errors that occurred since the last call of write() method 170 | * 171 | * @return integer 172 | */ 173 | public function getLastWriteErrorCount(); 174 | 175 | /** 176 | * Gets the array of errors (elements are of type RError) 177 | * that occurred since the last call of write() method 178 | * 179 | * @return array 180 | */ 181 | public function getLastWriteErrors(); 182 | 183 | /** 184 | * Check if R process is currently sensitive to errors 185 | * (throws RErrorsException when write() is called) 186 | */ 187 | public function isErrorSensitive(); 188 | 189 | /** 190 | * Sets sensitivity to R errors 191 | * If enabled, write() throws RErrorsException if they occur 192 | * Otherwise, the errors are just logged and are accessible via getLastWriteErrors() 193 | * 194 | * @param bool $trueOrFalse 195 | */ 196 | public function setErrorSensitive($trueOrFalse); 197 | } 198 | -------------------------------------------------------------------------------- /src/Kachkaev/PHPR/Process/AbstractRProcess.php: -------------------------------------------------------------------------------- 1 | mustNotBeRunning(); 30 | $this->inputLineCount = 0; 31 | $this->inputLog = array(); 32 | $this->outputLog = array(); 33 | $this->errors = array(); 34 | $this->lastWriteCommandCount = 0; 35 | $this->lastWriteErrorCount = 0; 36 | 37 | $this->doStart(); 38 | $this->isRunning = true; 39 | } 40 | 41 | public function stop() 42 | { 43 | $this->mustBeRunning(); 44 | $this->doStop(); 45 | $this->isRunning = false; 46 | } 47 | 48 | public function restart() 49 | { 50 | $this->stop(); 51 | $this->start(); 52 | } 53 | 54 | public function isRunning() 55 | { 56 | return !$this->isRunning; 57 | } 58 | 59 | public function write($rInput) 60 | { 61 | if (!is_string($rInput)) { 62 | throw new \InvalidArgumentException( 63 | sprintf("R input must be a string, %s given", 64 | var_export($rInput, true))); 65 | } 66 | 67 | $this->mustBeRunning(); 68 | 69 | $this->lastWriteCommandCount = 0; 70 | $this->lastWriteErrorCount = 0; 71 | 72 | $cachedAllResultAsString = null; 73 | $cachedLastWriteResultAsString = null; 74 | $cachedAllResultAsArray = null; 75 | $cachedLastWriteResultAsArray = null; 76 | 77 | try { 78 | $rInputLines = explode("\n", $rInput); 79 | $this->doWrite($rInputLines); 80 | } catch (Exception $e) { 81 | try { 82 | $this->stop(); 83 | } catch (Exception $e) { 84 | } 85 | throw $e; 86 | } 87 | 88 | $errorCount = $this->getLastWriteErrorCount(); 89 | if ($this->errorSensitive && $errorCount) { 90 | throw new RErrorsException($this->getLastWriteInput(true), $this->getLastWriteOutput(true), $this->getLastWriteErrors()); 91 | }; 92 | 93 | return $errorCount; 94 | } 95 | 96 | public function getAllInput($asArray = false) 97 | { 98 | return $asArray ? $this->inputLog : implode("\n", $this->inputLog); 99 | } 100 | 101 | public function getAllOutput($asArray = false) 102 | { 103 | return $asArray ? $this->outputLog : implode("\n", $this->outputLog); 104 | } 105 | 106 | public function getAllResult($asArray = false) 107 | { 108 | $commandCount = count($this->inputLog); 109 | 110 | if ($commandCount == 0) { 111 | return $asArray ? array() : ''; 112 | } 113 | ; 114 | 115 | if ($asArray) { 116 | if (!$this->cachedAllResultAsArray) { 117 | $this->cachedAllResultAsArray = $this 118 | ->getResult(true, 0, $commandCount - 1); 119 | } 120 | return $this->cachedAllResultAsArray; 121 | } else { 122 | if (!$this->cachedAllResultAsString) { 123 | $this->cachedAllResultAsString = $this 124 | ->getResult(false, 0, $commandCount - 1); 125 | } 126 | return $this->cachedAllResultAsString; 127 | } 128 | } 129 | 130 | public function getLastWriteInput($asArray = false) 131 | { 132 | $lastWriteInput = array_slice($this->inputLog, 133 | -$this->lastWriteCommandCount, $this->lastWriteCommandCount); 134 | return $asArray ? $lastWriteInput : implode("\n", $lastWriteInput); 135 | } 136 | 137 | public function getLastWriteOutput($asArray = false) 138 | { 139 | $lastWriteOutput = array_slice($this->outputLog, 140 | -$this->lastWriteCommandCount, $this->lastWriteCommandCount); 141 | return $asArray ? $lastWriteOutput : implode("\n", $lastWriteOutput); 142 | } 143 | 144 | public function getLastWriteResult($asArray = false) 145 | { 146 | if ($this->lastWriteCommandCount) { 147 | return $asArray ? array() : ''; 148 | } 149 | 150 | $commandCount = count($this->inputLog); 151 | if ($asArray) { 152 | if (!$this->cachedAllResultAsArray) { 153 | $this->cachedLastWriteResultAsArray = $this 154 | ->getResult(true, 155 | $commandCount - $this->lastWriteCommandCount, 156 | $commandCount - 1); 157 | } 158 | return $this->cachedLastWriteResultAsArray; 159 | } else { 160 | if (!$this->cachedLastWriteResultAsString) { 161 | $this->cachedLastWriteResultAsString = $this 162 | ->getResult(false, 163 | $commandCount - $this->lastWriteCommandCount, 164 | $commandCount - 1); 165 | } 166 | return $this->cachedLastWriteResultAsString; 167 | } 168 | } 169 | 170 | public function hasErrors() 171 | { 172 | return count($this->errors) != 0; 173 | } 174 | 175 | public function getErrorCount() 176 | { 177 | return count($this->errors); 178 | } 179 | 180 | public function getErrors() 181 | { 182 | return $this->errors; 183 | } 184 | 185 | public function hasLastWriteErrors() 186 | { 187 | return $this->lastWriteErrorCount != 0; 188 | } 189 | 190 | public function getLastWriteErrorCount() 191 | { 192 | return $this->lastWriteErrorCount; 193 | } 194 | 195 | public function getLastWriteErrors() 196 | { 197 | $lastWriteErrors = array_slice($this->errors, 198 | -$this->lastWriteErrorCount, $this->lastWriteErrorCount); 199 | return $lastWriteErrors; 200 | 201 | } 202 | 203 | public function isErrorSensitive() 204 | { 205 | return $this->errorSensitive; 206 | } 207 | 208 | public function setErrorSensitive($trueOrFalse) 209 | { 210 | if (!is_bool($trueOrFalse)) { 211 | throw new \InvalidArgumentException( 212 | sprintf( 213 | 'New value of error sensitivity must be boolean, %s given', 214 | var_export($trueOrFalse, true))); 215 | } 216 | $this->errorSensitive = $trueOrFalse; 217 | } 218 | 219 | private function mustBeRunning() 220 | { 221 | if (!$this->isRunning) { 222 | throw new RProcessException( 223 | 'R process is stopped, it must be started'); 224 | } 225 | } 226 | 227 | private function mustNotBeRunning() 228 | { 229 | if ($this->isRunning) { 230 | throw new RProcessException( 231 | 'R process has been started, it must be stopped'); 232 | } 233 | } 234 | 235 | /** 236 | * @see AbstractRProcess::getAllResult() 237 | */ 238 | private function getResult($asArray, $commandNumberFrom, $commandNumberTo) 239 | { 240 | if (!is_int($commandNumberFrom) || !is_int($commandNumberTo) 241 | || $commandNumberFrom < 0 242 | || $commandNumberTo >= count($this->inputLog) 243 | || $commandNumberFrom > $commandNumberTo) { 244 | throw new \InvalidArgumentException( 245 | sprintf('Wrong command range: %s, %s', 246 | var_export($commandNumberFrom, true), 247 | var_export($commandNumberTo, true))); 248 | } 249 | 250 | $errorsByCommandNumbers = array(); 251 | 252 | foreach ($this->errors as $error) { 253 | $n = $error->getCommandNumber(); 254 | if ($n >= $commandNumberFrom && $n <= $commandNumberTo) { 255 | $errorsByCommandNumbers[$n] = $error; 256 | } 257 | } 258 | 259 | $resultAsArray = array(); 260 | for ($n = $commandNumberFrom; $n <= $commandNumberTo; ++$n) { 261 | $errorMessage = null; 262 | if (array_key_exists($n, $errorsByCommandNumbers)) { 263 | $errorMessage = $errorsByCommandNumbers[$n]->getErrorMessage(); 264 | } 265 | $resultAsArray[] = array($this->inputLog[$n], $this->outputLog[$n], 266 | $errorMessage); 267 | } 268 | 269 | if ($asArray) { 270 | return $resultAsArray; 271 | } 272 | 273 | $resultbyCommands = array(); 274 | 275 | foreach ($resultAsArray as $resultCommand) { 276 | $in = '> ' . str_replace("\n", "\n+ ", $resultCommand[0]); 277 | $out = $resultCommand[2] ? : $resultCommand[1]; 278 | 279 | if (strlen($out)) { 280 | $resultByCommands[] = $in . "\n" . $out; 281 | } else { 282 | $resultByCommands[] = $in; 283 | } 284 | } 285 | 286 | return implode("\n", $resultByCommands); 287 | } 288 | } 289 | --------------------------------------------------------------------------------