diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c1fc0c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor +composer.phar +composer.lock +.DS_Store \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..cf7464d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: php + +php: + - 5.3 + - 5.4 + +before_script: + - curl -s http://getcomposer.org/installer | php + - php composer.phar install --dev + +script: phpunit \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6b0275a --- /dev/null +++ b/composer.json @@ -0,0 +1,23 @@ +{ + "name": "kodebyraaet/prince", + "description": "Simple prince wrapper for Prince library", + "authors": [ + { + "name": "Anthoni Giskegjerde", + "email": "anthoni@kodebyraaet.no" + }, + { + "name": "Jon Myrstad", + "email": "jon@kodebyraaet.no" + } + ], + "require": { + "php": ">=5.3.0", + "illuminate/support": "4.2.x" + }, + "autoload": { + "psr-4": { + "Kodebyraaet\\Prince\\": "src/" + } + } +} \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e89ac6d --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests/ + + + \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..e7d3cd3 --- /dev/null +++ b/readme.md @@ -0,0 +1,53 @@ +# Laravel PrinceXML Wrapper + +Laravel PrinceXML Wrapper is a Laravel 4.2 package that wraps around the http://www.princexml.com/ PDF generator. + +## Installation +There's a requirement to have the "prince" executable installed. +``` +Install the "prince" executable from http://www.princexml.com/download/. +``` +Run the following command to add Kodebyraaet/Prince to download and install. +``` +composer require kodebyraaet/prince +``` +Add the following .env variable to your .env.*.php. +``` +PRINCE_EXECUTABLE_PATH=/path/to/prince +``` +Add the following to your app.php file in the service provider and alias sections respectively. The alias/facade is optional. +```` +'Kodebyraaet\Prince\PrinceServiceProvider', +'Prince' => 'Kodebyraaet\Prince\Facades\Prince', +```` + +# Usage +The Kodebyraaet\Prince\Prince class is bound in the Laravel IoC as a Kodebyraaet\Prince\PrinceInterface, so everywhere the IoC automatically resolves dependencies (ie. in controllers) this is the preferred way to use Prince. Optionally you can also use $app->make(...) or App::make(...); You can also use \Prince or Kodebyraaet\Prince\Facades\Prince directly anywhere. + +## Methods +All methods are chainable so you can dynamically add more and more markup as you go to a Prince document. For example: +``` +$prince->html('') + ->html('

Appending more content.

+ ->html(''); +``` + +The *html* method takes html as a string and appends it to the internally stored markup. +``` +$prince->html('
Some HTML
'); +``` + +The *view* method takes a Laravel view that has not yet been rendered and renders is and appends it to the internally stored markup. +``` +$prince->view(View::make('someview',['somevar' => $somevalue])); +``` + +The *download* method returns a Response object that can be returned to the client for view in browser/download. For example, in a Controller you can return this for a direct view of a generated PDF. +``` +return $prince->html('...')->download(); +``` + +The *store* method required a path and returns the same path if successful. +``` +$pdfPath = $prince->html('...')->store(public_path('/pdf/example.pdf')); +``` \ No newline at end of file diff --git a/src/Kodebyraaet/Prince/Exceptions/PrinceError.php b/src/Kodebyraaet/Prince/Exceptions/PrinceError.php new file mode 100644 index 0000000..97935eb --- /dev/null +++ b/src/Kodebyraaet/Prince/Exceptions/PrinceError.php @@ -0,0 +1,8 @@ +app = $app; + + $this->executable = getenv('PRINCE_EXECUTABLE_PATH') ? getenv('PRINCE_EXECUTABLE_PATH') : '/usr/bin/prince'; + + $this->arguments = ' --server -i "html" --silent -'; + + $this->displayErrors = true; + + $this->useUtf8 = false; + } + + /** + * Takes a View and returns $this to further chain methods on. + * + * @param View $view + * @return $this + */ + public function view(View $view) + { + $this->markup .= $view->render(); + + return $this; + } + + /** + * Takes a HTML string and returns $this to further chain methods on. + * + * @param $html + * @return $this + */ + public function html($html) + { + $this->markup .= $html; + + return $this; + } + + /** + * Returns the generated PDF as as a Response. + * + * @return Response + */ + public function download() + { + $data = $this->generate(); + + $this->reset(); + + return ResponseFacade::stream(function () use ($data) { + $out = fopen('php://output', 'w'); + fputs($out, $data); + fclose($out); + }, 200, [ + 'Content-Type' => 'application/pdf' + ]); + + } + + /** + * Stores the generated PDF in at a provided path. + * + * @param $path + * @return mixed + */ + public function store($path) + { + $data = $this->generate(); + + File::put($path, $data); + + $this->reset(); + + return $path; + } + + /** + * Does the actual generation of a PDF. Returns raw PDF data for further + * processing. + * + * @return string + * @throws UnableToExecute + */ + private function generate() + { + $markup = ($this->useUtf8) ? utf8_decode($this->markup) : $this->markup; + + // The different ways we want to communicate with the executable. + $descriptorSpec = array( + 0 => array("pipe", "r"), + 1 => array("pipe", "w"), + 2 => array("pipe", "w") + ); + + // Tries to open up a process of prince at $this->executable path, + // appends $this->arguments to the executable call. + if (!is_resource($process = proc_open($this->executable . $this->arguments, $descriptorSpec, $pipes))) { + throw new UnableToExecute('Unable to open a prince executable at path ' . $this->executable . '.'); + } + + // Write all the markup to the pipe we can write to + // and tell the executable we're done sending markup. + fwrite($pipes[0], $markup); + fclose($pipes[0]); + + // Get the pure response from the executable. + $rawData = stream_get_contents($pipes[1]); + fclose($pipes[1]); + + // Check if the executable gave us any errors. + $this->parseErrors($pipes[2]); + fclose($pipes[2]); + + // End the executable in it's entirety. + proc_close($process); + + $this->reset(); + + return $rawData; + } + + /** + * Checks if there are any errors in the pipeline. + * + * @param resource $pipe + * @throws PrinceError + * @return bool + */ + protected function parseErrors($pipe) + { + // While the executable is still piping data. + while (!feof($pipe)) { + $line = fgets($pipe); + + if ($line) { + $tag = substr($line, 0, 3); + $body = substr($line, 4); + + if ($tag == 'fin') { + // End of data. + return false; + } + + $messages[] = $body; + } + } + + // If we have any errors we throw a PrinceError. + if (isset($messages)) { + $errorMessage = 'Prince error: ' . implode(', ', $messages); + + throw new PrinceError($errorMessage); + } + + return true; + } + + /** + * Prepares the object for re-use. + */ + protected function reset() + { + $this->markup = ''; + } + +} diff --git a/src/Kodebyraaet/Prince/PrinceInterface.php b/src/Kodebyraaet/Prince/PrinceInterface.php new file mode 100644 index 0000000..f9ab3bb --- /dev/null +++ b/src/Kodebyraaet/Prince/PrinceInterface.php @@ -0,0 +1,40 @@ +app->bind('Kodebyraaet\Prince\PrinceInterface', function ($app) { + return new Prince($app); + }); + } + +} \ No newline at end of file diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29