Browse Source

Procedures are now compiled and cached from partials allowing for partial script injection

Jonny Wenmoth 10 năm trước cách đây
mục cha
commit
f939babff7
43 tập tin đã thay đổi với 1560 bổ sung956 xóa
  1. 11 1
      phpunit.xml.dist
  2. 4 2
      src/JonnyW/PhantomJs/Cache/FileCache.php
  3. 50 182
      src/JonnyW/PhantomJs/Client.php
  4. 21 62
      src/JonnyW/PhantomJs/ClientInterface.php
  5. 221 0
      src/JonnyW/PhantomJs/Engine.php
  6. 2 2
      src/JonnyW/PhantomJs/Http/RequestInterface.php
  7. 29 2
      src/JonnyW/PhantomJs/Procedure/ChainProcedureLoader.php
  8. 15 6
      src/JonnyW/PhantomJs/Procedure/Procedure.php
  9. 161 0
      src/JonnyW/PhantomJs/Procedure/ProcedureCompiler.php
  10. 60 0
      src/JonnyW/PhantomJs/Procedure/ProcedureCompilerInterface.php
  11. 16 4
      src/JonnyW/PhantomJs/Procedure/ProcedureFactory.php
  12. 1 4
      src/JonnyW/PhantomJs/Procedure/ProcedureInterface.php
  13. 18 4
      src/JonnyW/PhantomJs/Procedure/ProcedureLoader.php
  14. 10 0
      src/JonnyW/PhantomJs/Procedure/ProcedureLoaderInterface.php
  15. 11 20
      src/JonnyW/PhantomJs/Procedure/ProcedureValidator.php
  16. 3 6
      src/JonnyW/PhantomJs/Procedure/ProcedureValidatorInterface.php
  17. 8 0
      src/JonnyW/PhantomJs/Resources/config/config.yml
  18. 44 0
      src/JonnyW/PhantomJs/Resources/config/services.yml
  19. 0 0
      src/JonnyW/PhantomJs/Resources/procedures/global_variables.partial
  20. 0 165
      src/JonnyW/PhantomJs/Resources/procedures/http_capture.proc
  21. 35 94
      src/JonnyW/PhantomJs/Resources/procedures/http_default.proc
  22. 21 0
      src/JonnyW/PhantomJs/Resources/procedures/page_clip_rect.partial
  23. 5 0
      src/JonnyW/PhantomJs/Resources/procedures/page_custom_headers.partial
  24. 12 0
      src/JonnyW/PhantomJs/Resources/procedures/page_on_error.partial
  25. 5 0
      src/JonnyW/PhantomJs/Resources/procedures/page_on_resource_received.partial
  26. 4 0
      src/JonnyW/PhantomJs/Resources/procedures/page_on_resource_timeout.partial
  27. 16 0
      src/JonnyW/PhantomJs/Resources/procedures/page_open.partial
  28. 3 0
      src/JonnyW/PhantomJs/Resources/procedures/page_settings.partial
  29. 14 0
      src/JonnyW/PhantomJs/Resources/procedures/page_viewport_size.partial
  30. 14 0
      src/JonnyW/PhantomJs/Resources/procedures/phantom_on_error.partial
  31. 25 0
      src/JonnyW/PhantomJs/Resources/procedures/procedure_capture.partial
  32. 23 0
      src/JonnyW/PhantomJs/Resources/procedures/procedure_default.partial
  33. 1 1
      src/JonnyW/PhantomJs/Resources/procedures/validator.proc
  34. 14 6
      src/JonnyW/PhantomJs/Tests/Integration/ClientTest.php
  35. 265 0
      src/JonnyW/PhantomJs/Tests/Integration/Procedure/ProcedureCompilerTest.php
  36. 5 130
      src/JonnyW/PhantomJs/Tests/Integration/Procedure/ProcedureValidatorTest.php
  37. 21 0
      src/JonnyW/PhantomJs/Tests/Unit/Cache/FileCacheTest.php
  38. 43 244
      src/JonnyW/PhantomJs/Tests/Unit/ClientTest.php
  39. 231 0
      src/JonnyW/PhantomJs/Tests/Unit/EngineTest.php
  40. 41 0
      src/JonnyW/PhantomJs/Tests/Unit/Procedure/ChainProcedureLoaderTest.php
  41. 19 3
      src/JonnyW/PhantomJs/Tests/Unit/Procedure/ProcedureFactoryTest.php
  42. 38 1
      src/JonnyW/PhantomJs/Tests/Unit/Procedure/ProcedureLoaderTest.php
  43. 20 17
      src/JonnyW/PhantomJs/Tests/Unit/Procedure/ProcedureTest.php

+ 11 - 1
phpunit.xml.dist

@@ -1,6 +1,16 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
-<phpunit bootstrap="./src/JonnyW/PhantomJs/Tests/bootstrap.php" colors="true">
+<phpunit 
+    bootstrap                   = "./src/JonnyW/PhantomJs/Tests/bootstrap.php" 
+    backupGlobals               = "false"
+    backupStaticAttributes      = "false"
+    colors                      = "true"
+    convertErrorsToExceptions   = "true"
+    convertNoticesToExceptions  = "true"
+    convertWarningsToExceptions = "true"
+    processIsolation            = "false"
+    stopOnFailure               = "true"
+    syntaxCheck                 = "false">
     <testsuites>
         <testsuite name="PhantomJS Test Suite">
             <directory suffix="Test.php">./src/JonnyW/PhantomJs/Tests/*</directory>

+ 4 - 2
src/JonnyW/PhantomJs/Cache/FileCache.php

@@ -102,8 +102,10 @@ class FileCache implements CacheInterface
      */
     public function delete($id)
     {
-        if ($this->exists($id)) {
-            unlink($this->getFilename($id));
+        $files = glob($this->getFilename($id));
+
+        if (count($files)) {
+            array_map('unlink', $files);
         }
     }
 

+ 50 - 182
src/JonnyW/PhantomJs/Client.php

@@ -8,9 +8,8 @@
  */
 namespace JonnyW\PhantomJs;
 
-use JonnyW\PhantomJs\Exception\InvalidExecutableException;
 use JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface;
-use JonnyW\PhantomJs\Procedure\ProcedureValidatorInterface;
+use JonnyW\PhantomJs\Procedure\ProcedureCompilerInterface;
 use JonnyW\PhantomJs\Http\MessageFactoryInterface;
 use JonnyW\PhantomJs\Http\RequestInterface;
 use JonnyW\PhantomJs\Http\ResponseInterface;
@@ -31,6 +30,14 @@ class Client implements ClientInterface
      */
     private static $instance;
 
+    /**
+     * PhantomJs engine.
+     *
+     * @var \JonnyW\PhantomJs\Engine
+     * @access protected
+     */
+    protected $engine;
+
     /**
      * Procedure loader.
      *
@@ -42,10 +49,10 @@ class Client implements ClientInterface
     /**
      * Procedure validator.
      *
-     * @var \JonnyW\PhantomJs\Procedure\ProcedureValidatorInterface
+     * @var \JonnyW\PhantomJs\Procedure\ProcedureCompilerInterface
      * @access protected
      */
-    protected $procedureValidator;
+    protected $procedureCompiler;
 
     /**
      * Message factory.
@@ -56,53 +63,30 @@ class Client implements ClientInterface
     protected $messageFactory;
 
     /**
-     * Path to PhantomJs executable
+     * Procedure template
      *
      * @var string
      * @access protected
      */
-    protected $phantomJs;
-
-    /**
-     * Debug flag.
-     *
-     * @var boolean
-     * @access protected
-     */
-    protected $debug;
-
-    /**
-     * PhantomJs run options.
-     *
-     * @var array
-     * @access protected
-     */
-    protected $options;
-
-    /**
-     * Log info
-     *
-     * @var string
-     * @access protected
-     */
-    protected $log;
+    protected $procedure;
 
     /**
      * Internal constructor
      *
      * @access public
-     * @param  \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface    $procedureLoader
-     * @param  \JonnyW\PhantomJs\Procedure\ProcedureValidatorInterface $procedureValidator
-     * @param  \JonnyW\PhantomJs\Http\MessageFactoryInterface          $messageFactory
+     * @param  \JonnyW\PhantomJs\Engine                               $engine
+     * @param  \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface   $procedureLoader
+     * @param  \JonnyW\PhantomJs\Procedure\ProcedureCompilerInterface $procedureCompiler
+     * @param  \JonnyW\PhantomJs\Http\MessageFactoryInterface         $messageFactory
      * @return void
      */
-    public function __construct(ProcedureLoaderInterface $procedureLoader, ProcedureValidatorInterface $procedureValidator, MessageFactoryInterface $messageFactory)
+    public function __construct(Engine $engine, ProcedureLoaderInterface $procedureLoader, ProcedureCompilerInterface $procedureCompiler, MessageFactoryInterface $messageFactory)
     {
-        $this->procedureLoader    = $procedureLoader;
-        $this->procedureValidator = $procedureValidator;
-        $this->messageFactory     = $messageFactory;
-        $this->phantomJs          = 'bin/phantomjs';
-        $this->options            = array();
+        $this->engine            = $engine;
+        $this->procedureLoader   = $procedureLoader;
+        $this->procedureCompiler = $procedureCompiler;
+        $this->messageFactory    = $messageFactory;
+        $this->procedure         = 'http_default';
     }
 
     /**
@@ -118,8 +102,9 @@ class Client implements ClientInterface
             $serviceContainer = ServiceContainer::getInstance();
 
             self::$instance = new static(
+                $serviceContainer->get('engine'),
                 $serviceContainer->get('procedure_loader'),
-                $serviceContainer->get('procedure_validator'),
+                $serviceContainer->get('procedure_compiler'),
                 $serviceContainer->get('message_factory')
             );
         }
@@ -127,6 +112,17 @@ class Client implements ClientInterface
         return self::$instance;
     }
 
+    /**
+     * Get PhantomJs engine.
+     *
+     * @access public
+     * @return \JonnyW\PhantomJs\Engine
+     */
+    public function getEngine()
+    {
+        return $this->engine;
+    }
+
     /**
      * Get message factory instance
      *
@@ -159,174 +155,46 @@ class Client implements ClientInterface
      */
     public function send(RequestInterface $request, ResponseInterface $response)
     {
-        $procedure = $this->procedureLoader->load($request->getType());
+        $procedure = $this->procedureLoader->load($this->procedure);
 
-        $this->procedureValidator->validate(
-            $this,
-            $procedure,
-            $request
-        );
+        $this->procedureCompiler->compile($procedure, $request);
 
-        $procedure->run($this, $request, $response);
+        $procedure->run($request, $response);
 
         return $response;
     }
 
     /**
-     * Get PhantomJs run command with
-     * loader and run options.
+     * Get log.
      *
      * @access public
      * @return string
      */
-    public function getCommand()
-    {
-        $phantomJs = $this->getPhantomJs();
-        $options   = $this->getOptions();
-
-        $this->validateExecutable($phantomJs);
-
-        if ($this->debug) {
-            array_push($options, '--debug=true');
-        }
-
-        return sprintf('%s %s', $phantomJs, implode(' ', $options));
-    }
-
-    /**
-     * Set new PhantomJs executable path.
-     *
-     * @access public
-     * @param  string                   $path
-     * @return \JonnyW\PhantomJs\Client
-     */
-    public function setPhantomJs($path)
-    {
-        $this->validateExecutable($path);
-
-        $this->phantomJs = $path;
-
-        return $this;
-    }
-
-    /**
-     * Get PhantomJs executable path.
-     *
-     * @access public
-     * @return string
-     */
-    public function getPhantomJs()
-    {
-        return $this->phantomJs;
-    }
-
-    /**
-     * Set PhantomJs run options.
-     *
-     * @access public
-     * @param  array                    $options
-     * @return \JonnyW\PhantomJs\Client
-     */
-    public function setOptions(array $options)
-    {
-        $this->options = $options;
-
-        return $this;
-    }
-
-    /**
-     * Get PhantomJs run options.
-     *
-     * @access public
-     * @return array
-     */
-    public function getOptions()
-    {
-        return (array) $this->options;
-    }
-
-    /**
-     * Add single PhantomJs run option.
-     *
-     * @access public
-     * @param  string                   $option
-     * @return \JonnyW\PhantomJs\Client
-     */
-    public function addOption($option)
-    {
-        if (!in_array($option, $this->options)) {
-            $this->options[] = $option;
-        }
-
-        return $this;
-    }
-
-    /**
-     * Debug.
-     *
-     * @access public
-     * @param  boolean                  $doDebug
-     * @return \JonnyW\PhantomJs\Client
-     */
-    public function debug($doDebug)
+    public function getLog()
     {
-        $this->debug = $doDebug;
-
-        return $this;
+        return $this->getEngine()->getLog();
     }
 
     /**
-     * Log info.
+     * Set procedure template.
      *
      * @access public
-     * @param  string                   $info
-     * @return \JonnyW\PhantomJs\Client
+     * @param  string $procedure
+     * @return void
      */
-    public function log($info)
+    public function setProcedure($procedure)
     {
-        $this->log = $info;
-
-        return $this;
+        $this->procedure = $procedure;
     }
 
     /**
-     * Get log info.
+     * Get procedure template.
      *
      * @access public
      * @return string
      */
-    public function getLog()
+    public function getProcedure()
     {
-        return $this->log;
-    }
-
-    /**
-     * Clear log info.
-     *
-     * @access public
-     * @return \JonnyW\PhantomJs\Client
-     */
-    public function clearLog()
-    {
-        $this->log = '';
-
-        return $this;
-    }
-
-    /**
-     * Validate execuable file.
-     *
-     * @access private
-     * @param  string                                                 $file
-     * @return boolean
-     * @throws \JonnyW\PhantomJs\Exception\InvalidExecutableException
-     */
-    private function validateExecutable($file)
-    {
-        if (!file_exists($file) || !is_executable($file)) {
-            throw new InvalidExecutableException(sprintf('File does not exist or is not executable: %s', $file));
-        }
-
-        return true;
+        return $this->procedure;
     }
 }

+ 21 - 62
src/JonnyW/PhantomJs/ClientInterface.php

@@ -27,101 +27,60 @@ interface ClientInterface
     public static function getInstance();
 
     /**
-     * Get message factory instance
+     * Get engine instance.
      *
      * @access public
-     * @return \JonnyW\PhantomJs\Http\MessageFactoryInterface
+     * @return \JonnyW\PhantomJs\Engine
      */
-    public function getMessageFactory();
+    public function getEngine();
 
     /**
-     * Send request
+     * Get message factory instance
      *
      * @access public
-     * @param \JonnyW\PhantomJs\Http\RequestInterface  $request
-     * @param \JonnyW\PhantomJs\Http\ResponseInterface $response
+     * @return \JonnyW\PhantomJs\Http\MessageFactoryInterface
      */
-    public function send(RequestInterface $request, ResponseInterface $response);
+    public function getMessageFactory();
 
     /**
-     * Get PhantomJs run command with
-     * loader and run options.
+     * Get procedure loader instance
      *
      * @access public
-     * @return string
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface
      */
-    public function getCommand();
+    public function getProcedureLoader();
 
     /**
-     * Set new PhantomJs executable path.
+     * Send request
      *
      * @access public
-     * @param string $path
+     * @param \JonnyW\PhantomJs\Http\RequestInterface  $request
+     * @param \JonnyW\PhantomJs\Http\ResponseInterface $response
      */
-    public function setPhantomJs($path);
+    public function send(RequestInterface $request, ResponseInterface $response);
 
     /**
-     * Get PhantomJs executable path.
+     * Get log.
      *
      * @access public
      * @return string
      */
-    public function getPhantomJs();
-
-    /**
-     * Set PhantomJs run options.
-     *
-     * @access public
-     * @param array $options
-     */
-    public function setOptions(array $options);
-
-    /**
-     * Get PhantomJs run options.
-     *
-     * @access public
-     * @return array
-     */
-    public function getOptions();
-
-    /**
-     * Add single PhantomJs run option.
-     *
-     * @access public
-     * @param string $option
-     */
-    public function addOption($option);
-
-    /**
-     * Debug.
-     *
-     * @access public
-     * @param boolean $doDebug
-     */
-    public function debug($doDebug);
+    public function getLog();
 
     /**
-     * Log info.
+     * Set procedure template.
      *
      * @access public
-     * @param  string                   $info
-     * @return \JonnyW\PhantomJs\Client
+     * @param  string $procedure
+     * @return void
      */
-    public function log($info);
+    public function setProcedure($procedure);
 
     /**
-     * Get log info.
+     * Get procedure template.
      *
      * @access public
      * @return string
      */
-    public function getLog();
-
-    /**
-     * Clear log info.
-     *
-     * @access public
-     * @return \JonnyW\PhantomJs\Client
-     */
-    public function clearLog();
+    public function getProcedure();
 }

+ 221 - 0
src/JonnyW/PhantomJs/Engine.php

@@ -0,0 +1,221 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs;
+
+use JonnyW\PhantomJs\Exception\InvalidExecutableException;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class Engine
+{
+    /**
+     * Executable path.
+     *
+     * @var string
+     * @access protected
+     */
+    protected $path;
+
+    /**
+     * Debug flag.
+     *
+     * @var boolean
+     * @access protected
+     */
+    protected $debug;
+
+    /**
+     * PhantomJs run options.
+     *
+     * @var array
+     * @access protected
+     */
+    protected $options;
+
+    /**
+     * Log info
+     *
+     * @var string
+     * @access protected
+     */
+    protected $log;
+
+    /**
+     * Internal constructor
+     *
+     * @access public
+     * @return void
+     */
+    public function __construct()
+    {
+        $this->path    = 'bin/phantomjs';
+        $this->options = array();
+    }
+
+    /**
+     * Get PhantomJs run command with
+     * loader run options.
+     *
+     * @access public
+     * @return string
+     */
+    public function getCommand()
+    {
+        $path    = $this->getPath();
+        $options = $this->getOptions();
+
+        $this->validateExecutable($path);
+
+        if ($this->debug) {
+            array_push($options, '--debug=true');
+        }
+
+        return sprintf('%s %s', $path, implode(' ', $options));
+    }
+
+    /**
+     * Set path.
+     *
+     * @access public
+     * @param  string                   $path
+     * @return \JonnyW\PhantomJs\Client
+     */
+    public function setPath($path)
+    {
+        $this->validateExecutable($path);
+
+        $this->path = $path;
+
+        return $this;
+    }
+
+    /**
+     * Get path.
+     *
+     * @access public
+     * @return string
+     */
+    public function getPath()
+    {
+        return $this->path;
+    }
+
+    /**
+     * Set PhantomJs run options.
+     *
+     * @access public
+     * @param  array                    $options
+     * @return \JonnyW\PhantomJs\Client
+     */
+    public function setOptions(array $options)
+    {
+        $this->options = $options;
+
+        return $this;
+    }
+
+    /**
+     * Get PhantomJs run options.
+     *
+     * @access public
+     * @return array
+     */
+    public function getOptions()
+    {
+        return (array) $this->options;
+    }
+
+    /**
+     * Add single PhantomJs run option.
+     *
+     * @access public
+     * @param  string                   $option
+     * @return \JonnyW\PhantomJs\Client
+     */
+    public function addOption($option)
+    {
+        if (!in_array($option, $this->options)) {
+            $this->options[] = $option;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Debug.
+     *
+     * @access public
+     * @param  boolean                  $doDebug
+     * @return \JonnyW\PhantomJs\Client
+     */
+    public function debug($doDebug)
+    {
+        $this->debug = $doDebug;
+
+        return $this;
+    }
+
+    /**
+     * Log info.
+     *
+     * @access public
+     * @param  string                   $info
+     * @return \JonnyW\PhantomJs\Client
+     */
+    public function log($info)
+    {
+        $this->log = $info;
+
+        return $this;
+    }
+
+    /**
+     * Get log info.
+     *
+     * @access public
+     * @return string
+     */
+    public function getLog()
+    {
+        return $this->log;
+    }
+
+    /**
+     * Clear log info.
+     *
+     * @access public
+     * @return \JonnyW\PhantomJs\Client
+     */
+    public function clearLog()
+    {
+        $this->log = '';
+
+        return $this;
+    }
+
+    /**
+     * Validate execuable file.
+     *
+     * @access private
+     * @param  string                                                 $file
+     * @return boolean
+     * @throws \JonnyW\PhantomJs\Exception\InvalidExecutableException
+     */
+    private function validateExecutable($file)
+    {
+        if (!file_exists($file) || !is_executable($file)) {
+            throw new InvalidExecutableException(sprintf('File does not exist or is not executable: %s', $file));
+        }
+
+        return true;
+    }
+}

+ 2 - 2
src/JonnyW/PhantomJs/Http/RequestInterface.php

@@ -24,8 +24,8 @@ interface RequestInterface
     const METHOD_DELETE  = 'DELETE';
     const METHOD_PATCH   = 'PATCH';
 
-    const REQUEST_TYPE_DEFAULT = 'http_default';
-    const REQUEST_TYPE_CAPTURE = 'http_capture';
+    const REQUEST_TYPE_DEFAULT = 'default';
+    const REQUEST_TYPE_CAPTURE = 'capture';
 
     /**
      * Get request type

+ 29 - 2
src/JonnyW/PhantomJs/Procedure/ChainProcedureLoader.php

@@ -43,7 +43,7 @@ class ChainProcedureLoader implements ProcedureLoaderInterface
      */
     public function addLoader(ProcedureLoaderInterface $procedureLoader)
     {
-        $this->procedureLoaders[] = $procedureLoader;
+        array_unshift($this->procedureLoaders, $procedureLoader);
     }
 
     /**
@@ -56,7 +56,7 @@ class ChainProcedureLoader implements ProcedureLoaderInterface
      */
     public function load($id)
     {
-        /** @var \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface $loader */
+        /** @var \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface $loader **/
         foreach ($this->procedureLoaders as $loader) {
 
             try {
@@ -71,4 +71,31 @@ class ChainProcedureLoader implements ProcedureLoaderInterface
 
         throw new \InvalidArgumentException(sprintf('No valid procedure loader could be found to load the \'%s\' procedure.', $id));
     }
+
+    /**
+     * Load procedure template by id.
+     *
+     * @access public
+     * @param  string                    $id
+     * @param  string                    $extension (default: 'proc')
+     * @throws \InvalidArgumentException
+     * @return string
+     */
+    public function loadTemplate($id, $extension = 'proc')
+    {
+        /** @var \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface $loader **/
+        foreach ($this->procedureLoaders as $loader) {
+
+            try {
+
+                $template = $loader->loadTemplate($id, $extension);
+
+                return $template;
+
+            } catch (\Exception $e) {}
+
+        }
+
+        throw new \InvalidArgumentException(sprintf('No valid procedure loader could be found to load the \'%s\' procedure template.', $id));
+    }
 }

+ 15 - 6
src/JonnyW/PhantomJs/Procedure/Procedure.php

@@ -8,7 +8,7 @@
  */
 namespace JonnyW\PhantomJs\Procedure;
 
-use JonnyW\PhantomJs\ClientInterface;
+use JonnyW\PhantomJs\Engine;
 use JonnyW\PhantomJs\Cache\CacheInterface;
 use JonnyW\PhantomJs\Parser\ParserInterface;
 use JonnyW\PhantomJs\Template\TemplateRendererInterface;
@@ -22,6 +22,14 @@ use JonnyW\PhantomJs\Exception\ProcedureFailedException;
  */
 class Procedure implements ProcedureInterface
 {
+    /**
+     * PhantomJS engine
+     *
+     * @var \JonnyW\PhantomJs\Engine
+     * @access protected
+     */
+    protected $engine;
+
     /**
      * Parser instance.
      *
@@ -58,12 +66,14 @@ class Procedure implements ProcedureInterface
      * Internal constructor.
      *
      * @access public
+     * @param \JonnyW\PhantomJs\Engine                             $engine
      * @param \JonnyW\PhantomJs\Parser\ParserInterface             $parser
      * @param \JonnyW\PhantomJs\Cache\CacheInterface               $cacheHandler
      * @param \JonnyW\PhantomJs\Template\TemplateRendererInterface $renderer
      */
-    public function __construct(ParserInterface $parser, CacheInterface $cacheHandler, TemplateRendererInterface $renderer)
+    public function __construct(Engine $engine, ParserInterface $parser, CacheInterface $cacheHandler, TemplateRendererInterface $renderer)
     {
+        $this->engine       = $engine;
         $this->parser       = $parser;
         $this->cacheHandler = $cacheHandler;
         $this->renderer     = $renderer;
@@ -73,14 +83,13 @@ class Procedure implements ProcedureInterface
      * Run procedure.
      *
      * @access public
-     * @param  \JonnyW\PhantomJs\ClientInterface                    $client
      * @param  \JonnyW\PhantomJs\Procedure\InputInterface           $input
      * @param  \JonnyW\PhantomJs\Procedure\OutputInterface          $output
      * @throws \JonnyW\PhantomJs\Exception\ProcedureFailedException
      * @throws \JonnyW\PhantomJs\Exception\NotWritableException
      * @return void
      */
-    public function run(ClientInterface $client, InputInterface $input, OutputInterface $output)
+    public function run(InputInterface $input, OutputInterface $output)
     {
         try {
 
@@ -94,7 +103,7 @@ class Procedure implements ProcedureInterface
                 array('pipe', 'w')
             );
 
-            $process = proc_open(escapeshellcmd(sprintf('%s %s', $client->getCommand(), $executable)), $descriptorspec, $pipes, null, null);
+            $process = proc_open(escapeshellcmd(sprintf('%s %s', $this->engine->getCommand(), $executable)), $descriptorspec, $pipes, null, null);
 
             if (!is_resource($process)) {
                 throw new ProcedureFailedException('proc_open() did not return a resource');
@@ -113,7 +122,7 @@ class Procedure implements ProcedureInterface
                 $this->parser->parse($result)
             );
 
-            $client->log($log);
+            $this->engine->log($log);
 
             $this->remove($executable);
 

+ 161 - 0
src/JonnyW/PhantomJs/Procedure/ProcedureCompiler.php

@@ -0,0 +1,161 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace JonnyW\PhantomJs\Procedure;
+
+use JonnyW\PhantomJs\Cache\CacheInterface;
+use JonnyW\PhantomJs\Template\TemplateRendererInterface;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ProcedureCompiler implements ProcedureCompilerInterface
+{
+    /**
+     * Procedure loader
+     *
+     * @var \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface
+     * @access protected
+     */
+    protected $procedureLoader;
+
+    /**
+     * Procedure validator
+     *
+     * @var \JonnyW\PhantomJs\Procedure\ProcedureValidatorInterface
+     * @access protected
+     */
+    protected $procedureValidator;
+
+    /**
+     * Cache handler
+     *
+     * @var \JonnyW\PhantomJs\Cache\CacheInterface
+     * @access protected
+     */
+    protected $cacheHandler;
+
+    /**
+     * Renderer
+     *
+     * @var \JonnyW\PhantomJs\Template\TemplateRendererInterface
+     * @access protected
+     */
+    protected $renderer;
+
+    /**
+     * Cache enabled
+     *
+     * @var boolean
+     * @access protected
+     */
+    protected $cacheEnabled;
+
+    /**
+     * Internal constructor
+     *
+     * @access public
+     * @param \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface    $procedureLoader
+     * @param \JonnyW\PhantomJs\Procedure\ProcedureValidatorInterface $procedureValidator
+     * @param \JonnyW\PhantomJs\Cache\CacheInterface                  $cacheHandler
+     * @param \JonnyW\PhantomJs\Template\TemplateRendererInterface    $renderer
+     */
+    public function __construct(ProcedureLoaderInterface $procedureLoader, ProcedureValidatorInterface $procedureValidator,
+        CacheInterface $cacheHandler, TemplateRendererInterface $renderer)
+    {
+        $this->procedureLoader    = $procedureLoader;
+        $this->procedureValidator = $procedureValidator;
+        $this->cacheHandler       = $cacheHandler;
+        $this->renderer           = $renderer;
+        $this->cacheEnabled       = true;
+    }
+
+    /**
+     * Compile partials into procedure.
+     *
+     * @access public
+     * @param  \JonnyW\PhantomJs\Procedure\ProcedureInterface $procedure
+     * @param  \JonnyW\PhantomJs\Procedure\InputInterface     $input
+     * @return void
+     */
+    public function compile(ProcedureInterface $procedure, InputInterface $input)
+    {
+        $cacheKey = sprintf('phantomjs_%s_%s', $input->getType(), md5($procedure->getTemplate()));
+
+        if ($this->cacheEnabled && $this->cacheHandler->exists($cacheKey)) {
+            $template = $this->cacheHandler->fetch($cacheKey);
+        }
+
+        if (empty($template)) {
+
+            $template  = $this->renderer
+                ->render($procedure->getTemplate(), array('engine' => $this, 'procedure_type' => $input->getType()));
+
+            $test = clone $procedure;
+            $test->setTemplate($template);
+
+            $compiled = $test->compile($input);
+
+            $this->procedureValidator->validate($compiled);
+
+            if ($this->cacheEnabled) {
+                $this->cacheHandler->save($cacheKey, $template);
+            }
+        }
+
+        $procedure->setTemplate($template);
+    }
+
+    /**
+     * Load partial template.
+     *
+     * @access public
+     * @param  string $name
+     * @return string
+     */
+    public function load($name)
+    {
+        return $this->procedureLoader->loadTemplate($name, 'partial');
+    }
+
+    /**
+     * Enable cache.
+     *
+     * @access public
+     * @return void
+     */
+    public function enableCache()
+    {
+        $this->cacheEnabled = true;
+    }
+
+    /**
+     * Disable cache.
+     *
+     * @access public
+     * @return void
+     */
+    public function disableCache()
+    {
+        $this->cacheEnabled = false;
+    }
+
+    /**
+     * Clear cache.
+     *
+     * @access public
+     * @return void
+     */
+    public function clearCache()
+    {
+        $this->cacheHandler->delete('phantomjs_*');
+    }
+}

+ 60 - 0
src/JonnyW/PhantomJs/Procedure/ProcedureCompilerInterface.php

@@ -0,0 +1,60 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace JonnyW\PhantomJs\Procedure;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+interface ProcedureCompilerInterface
+{
+    /**
+     * Compile partials into procedure.
+     *
+     * @access public
+     * @param \JonnyW\PhantomJs\Procedure\ProcedureInterface $procedure
+     * @param \JonnyW\PhantomJs\Procedure\InputInterface     $input
+     */
+    public function compile(ProcedureInterface $procedure, InputInterface $input);
+
+    /**
+     * Load partial template.
+     *
+     * @access public
+     * @param  string $name
+     * @return string
+     */
+    public function load($name);
+
+    /**
+     * Enable cache.
+     *
+     * @access public
+     * @return void
+     */
+    public function enableCache();
+
+    /**
+     * Disable cache.
+     *
+     * @access public
+     * @return void
+     */
+    public function disableCache();
+
+    /**
+     * Clear cache.
+     *
+     * @access public
+     * @return void
+     */
+    public function clearCache();
+}

+ 16 - 4
src/JonnyW/PhantomJs/Procedure/ProcedureFactory.php

@@ -9,9 +9,10 @@
 
 namespace JonnyW\PhantomJs\Procedure;
 
-use \JonnyW\PhantomJs\Cache\CacheInterface;
-use \JonnyW\PhantomJs\Parser\ParserInterface;
-use \JonnyW\PhantomJs\Template\TemplateRendererInterface;
+use JonnyW\PhantomJs\Engine;
+use JonnyW\PhantomJs\Cache\CacheInterface;
+use JonnyW\PhantomJs\Parser\ParserInterface;
+use JonnyW\PhantomJs\Template\TemplateRendererInterface;
 
 /**
  * PHP PhantomJs
@@ -20,6 +21,14 @@ use \JonnyW\PhantomJs\Template\TemplateRendererInterface;
  */
 class ProcedureFactory implements ProcedureFactoryInterface
 {
+    /**
+     * PhantomJS engine
+     *
+     * @var \JonnyW\PhantomJs\Engine
+     * @access protected
+     */
+    protected $engine;
+
     /**
      * Parser.
      *
@@ -48,12 +57,14 @@ class ProcedureFactory implements ProcedureFactoryInterface
      * Internal constructor.
      *
      * @access public
+     * @param \JonnyW\PhantomJs\Engine                             $engine
      * @param \JonnyW\PhantomJs\Parser\ParserInterface             $parser
      * @param \JonnyW\PhantomJs\Cache\CacheInterface               $cacheHandler
      * @param \JonnyW\PhantomJs\Template\TemplateRendererInterface $renderer
      */
-    public function __construct(ParserInterface $parser, CacheInterface $cacheHandler, TemplateRendererInterface $renderer)
+    public function __construct(Engine $engine, ParserInterface $parser, CacheInterface $cacheHandler, TemplateRendererInterface $renderer)
     {
+        $this->engine       = $engine;
         $this->parser       = $parser;
         $this->cacheHandler = $cacheHandler;
         $this->renderer     = $renderer;
@@ -68,6 +79,7 @@ class ProcedureFactory implements ProcedureFactoryInterface
     public function createProcedure()
     {
         $procedure = new Procedure(
+            $this->engine,
             $this->parser,
             $this->cacheHandler,
             $this->renderer

+ 1 - 4
src/JonnyW/PhantomJs/Procedure/ProcedureInterface.php

@@ -8,8 +8,6 @@
  */
 namespace JonnyW\PhantomJs\Procedure;
 
-use JonnyW\PhantomJs\ClientInterface;
-
 /**
  * PHP PhantomJs
  *
@@ -21,11 +19,10 @@ interface ProcedureInterface
      * Run procedure.
      *
      * @access public
-     * @param \JonnyW\PhantomJs\ClientInterface           $client
      * @param \JonnyW\PhantomJs\Procedure\InputInterface  $input
      * @param \JonnyW\PhantomJs\Procedure\OutputInterface $output
      */
-    public function run(ClientInterface $client, InputInterface $input, OutputInterface $output);
+    public function run(InputInterface $input, OutputInterface $output);
 
     /**
      * Set procedure template.

+ 18 - 4
src/JonnyW/PhantomJs/Procedure/ProcedureLoader.php

@@ -56,15 +56,29 @@ class ProcedureLoader implements ProcedureLoaderInterface
      */
     public function load($id)
     {
-        $path     = $this->locator->locate(sprintf('%s.proc', $id));
-        $template = $this->loadFile($path);
-
         $procedure = $this->procedureFactory->createProcedure();
-        $procedure->setTemplate($template);
+        $procedure->setTemplate(
+            $this->loadTemplate($id)
+        );
 
         return $procedure;
     }
 
+    /**
+     * Load procedure template by id.
+     *
+     * @access public
+     * @param  string $id
+     * @param  string $extension (default: 'proc')
+     * @return string
+     */
+    public function loadTemplate($id, $extension = 'proc')
+    {
+        $path = $this->locator->locate(sprintf('%s.%s', $id, $extension));
+
+        return $this->loadFile($path);
+    }
+
     /**
      * Load procedure file content.
      *

+ 10 - 0
src/JonnyW/PhantomJs/Procedure/ProcedureLoaderInterface.php

@@ -23,4 +23,14 @@ interface ProcedureLoaderInterface
      * @return \JonnyW\PhantomJs\Procedure\ProcedureInterface
      */
     public function load($id);
+
+    /**
+     * Load procedure template by id.
+     *
+     * @access public
+     * @param  string $id
+     * @param  string $extension (default: 'proc')
+     * @return string
+     */
+    public function loadTemplate($id, $extension = 'proc');
 }

+ 11 - 20
src/JonnyW/PhantomJs/Procedure/ProcedureValidator.php

@@ -8,7 +8,6 @@
  */
 namespace JonnyW\PhantomJs\Procedure;
 
-use JonnyW\PhantomJs\ClientInterface;
 use JonnyW\PhantomJs\Validator\EngineInterface;
 use JonnyW\PhantomJs\Exception\SyntaxException;
 use JonnyW\PhantomJs\Exception\RequirementException;
@@ -53,20 +52,14 @@ class ProcedureValidator implements ProcedureValidatorInterface
      * Validate procedure.
      *
      * @access public
-     * @param  \JonnyW\PhantomJs\ClientInterface                        $client
-     * @param  \JonnyW\PhantomJs\Procedure\ProcedureInterface           $procedure
-     * @param  \JonnyW\PhantomJs\Procedure\InputInterface               $message
+     * @param  string                                                   $procedure
      * @return boolean
      * @throws \JonnyW\PhantomJs\Exception\ProcedureValidationException
      */
-    public function validate(ClientInterface $client, ProcedureInterface $procedure, InputInterface $message)
+    public function validate($procedure)
     {
-        $compiled = $procedure->compile(
-            $message
-        );
-
-        $this->validateSyntax($client, $compiled);
-        $this->validateRequirements($client, $compiled);
+        $this->validateSyntax($procedure);
+        $this->validateRequirements($procedure);
 
         return true;
     }
@@ -75,21 +68,20 @@ class ProcedureValidator implements ProcedureValidatorInterface
      * Validate syntax.
      *
      * @access protected
-     * @param  \JonnyW\PhantomJs\ClientInterface           $client
-     * @param  stromg                                      $compiled
+     * @param  string                                      $procedure
      * @return void
      * @throws \JonnyW\PhantomJs\Exception\SyntaxException
      */
-    protected function validateSyntax(ClientInterface $client, $compiled)
+    protected function validateSyntax($procedure)
     {
         $input  = new Input();
         $output = new Output();
 
-        $input->set('procedure', $compiled);
+        $input->set('procedure', $procedure);
         $input->set('engine', $this->engine->toString());
 
         $validator = $this->procedureLoader->load('validator');
-        $validator->run($client, $input, $output);
+        $validator->run($input, $output);
 
         $errors = $output->get('errors');
 
@@ -102,14 +94,13 @@ class ProcedureValidator implements ProcedureValidatorInterface
      * validateRequirements function.
      *
      * @access protected
-     * @param  \JonnyW\PhantomJs\ClientInterface                $client
-     * @param  stromg                                           $compiled
+     * @param  string                                           $procedure
      * @return void
      * @throws \JonnyW\PhantomJs\Exception\RequirementException
      */
-    protected function validateRequirements(ClientInterface $client, $compiled)
+    protected function validateRequirements($procedure)
     {
-        if (preg_match('/phantom\.exit\(/', $compiled, $matches) !== 1) {
+        if (preg_match('/phantom\.exit\(/', $procedure, $matches) !== 1) {
             throw new RequirementException('Your procedure must contain a \'phantom.exit(1);\' command to avoid the PhantomJS process hanging');
         }
     }

+ 3 - 6
src/JonnyW/PhantomJs/Procedure/ProcedureValidatorInterface.php

@@ -8,8 +8,6 @@
  */
 namespace JonnyW\PhantomJs\Procedure;
 
-use JonnyW\PhantomJs\ClientInterface;
-
 /**
  * PHP PhantomJs
  *
@@ -21,10 +19,9 @@ interface ProcedureValidatorInterface
      * Validate procedure.
      *
      * @access public
-     * @param  \JonnyW\PhantomJs\ClientInterface              $client
-     * @param  \JonnyW\PhantomJs\Procedure\ProcedureInterface $procedure
-     * @param  \JonnyW\PhantomJs\Procedure\InputInterface     $message
+     * @param  string                                                   $procedure
      * @return boolean
+     * @throws \JonnyW\PhantomJs\Exception\ProcedureValidationException
      */
-    public function validate(ClientInterface $client, ProcedureInterface $procedure, InputInterface $message);
+    public function validate($procedure);
 }

+ 8 - 0
src/JonnyW/PhantomJs/Resources/config/config.yml

@@ -10,6 +10,12 @@ parameters:
     phantomjs.validator_dir: "%phantomjs.resource_dir%/validators"
     phantomjs.validator_engine: "esprima-2.0.0.js"
 
+##############
+### ENGINE ###
+##############
+
+    phantomjs.engine.class: JonnyW\PhantomJs\Engine
+
 ##################
 ### PROCEDURES ###
 ##################
@@ -19,6 +25,7 @@ parameters:
     phantomjs.procedure.procedure_factory.class: JonnyW\PhantomJs\Procedure\ProcedureFactory
     phantomjs.procedure.procedure_loader_factory.class: JonnyW\PhantomJs\Procedure\ProcedureLoaderFactory
     phantomjs.procedure.procedure_validator.class: JonnyW\PhantomJs\Procedure\ProcedureValidator
+    phantomjs.procedure.procedure_compiler.class: JonnyW\PhantomJs\Procedure\ProcedureCompiler
 
 ############
 ### HTTP ###
@@ -44,6 +51,7 @@ parameters:
 
     phantomjs.template.template_renderer.class: JonnyW\PhantomJs\Template\TemplateRenderer
     phantomjs.twig.environment.class: Twig_Environment
+    phantomjs.twig.lexer.class: Twig_Lexer
     phantomjs.twig.string_loader.class: Twig_Loader_String
 
 ##################

+ 44 - 0
src/JonnyW/PhantomJs/Resources/config/services.yml

@@ -1,5 +1,15 @@
 services:
 
+##############
+### ENGINE ###
+##############
+
+    engine:
+        alias: phantomjs.engine
+
+    phantomjs.engine:
+        class: %phantomjs.engine.class%
+
 ##################
 ### PROCEDURES ###
 ##################
@@ -10,6 +20,9 @@ services:
     procedure_validator:
         alias: phantomjs.procedure.procedure_validator
 
+    procedure_compiler:
+        alias: phantomjs.procedure.procedure_compiler
+
     procedure_loader_factory:
         alias: phantomjs.procedure.procedure_loader_factory
 
@@ -35,6 +48,7 @@ services:
     phantomjs.procedure.procedure_factory:
         class: %phantomjs.procedure.procedure_factory.class%
         arguments:
+            - @engine
             - @phantomjs.parser.json_parser
             - @phantomjs.cache.file_cache
             - @phantomjs.template.template_renderer
@@ -46,6 +60,15 @@ services:
             - @phantomjs.procedure.procedure_factory
         public: false
 
+    phantomjs.procedure.procedure_compiler:
+        class: %phantomjs.procedure.procedure_compiler.class%
+        arguments:
+            - @procedure_loader
+            - @procedure_validator
+            - @phantomjs.cache.file_cache
+            - @phantomjs.procedure.template_renderer
+        public: false
+
     phantomjs.procedure.procedure_validator:
         class: %phantomjs.procedure.procedure_validator.class%
         arguments:
@@ -112,6 +135,27 @@ services:
         class: %phantomjs.twig.string_loader.class%
         public: false
 
+    phantomjs.procedure.template_renderer:
+        class: %phantomjs.template.template_renderer.class%
+        arguments:
+            - @phantomjs.procedure.twig.environment
+        public: false
+
+    phantomjs.procedure.twig.environment:
+        class: %phantomjs.twig.environment.class%
+        arguments:
+            - @phantomjs.twig.string_loader
+        public: false
+        calls:
+            - [ "setLexer", [ @phantomjs.procedure.twig.lexer ] ]
+
+    phantomjs.procedure.twig.lexer:
+        class: %phantomjs.twig.lexer.class%
+        arguments:
+            - @phantomjs.procedure.twig.environment
+            - { tag_variable: [ "[[", "]]" ], tag_comment: [ "[#", "#]" ], tag_block:  [ "[%", "%]" ], interpolation:  [ "#[", "]" ] }
+        public: false
+
 ##################
 ### RESOURCES ####
 ##################

+ 0 - 0
src/JonnyW/PhantomJs/Resources/procedures/global_variables.partial


+ 0 - 165
src/JonnyW/PhantomJs/Resources/procedures/http_capture.proc

@@ -1,165 +0,0 @@
-{% autoescape false %}
-
-/**
- * Set up page and script parameters.
- */
-var page       = require('webpage').create(),
-    system     = require('system'),
-    response   = {},
-    headers    = {{ input.getHeaders('json') }},
-    delay      = {{ input.getDelay() }},
-    top        = {{ input.getRectTop() }},
-    left       = {{ input.getRectLeft() }},
-    width      = {{ input.getRectWidth() }},
-    height     = {{ input.getRectHeight() }},
-    vpWidth    = {{ input.getViewportWidth() }},
-    vpHeight   = {{ input.getViewportHeight() }},
-    debug      = [],
-    logs       = [],
-    procedure  = {};
-
-
-/**
- * Define width & height of screenshot.
- */
-if(width && height) {
-    
-    debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Set capture clipping size ~ top: ' + top + ' left: ' + left + ' ' + width + 'x' + height);
-    
-    page.clipRect = {
-        top: top,
-        left: left,
-        width: width,
-        height: height
-    };
-}
-
-/**
- * Set viewport size.
- */
-if(vpWidth && vpHeight) {
-    
-    debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Set viewport size ~ width: ' + vpWidth + ' height: ' + vpHeight);
-    
-    page.viewportSize = {
-        width: vpWidth,
-        height: vpHeight
-    };
-}
-
-/**
- * Define custom headers.
- */
-page.customHeaders = headers ? headers : {};
-
-/**
- * Set timeout.
- */
-page.settings.resourceTimeout = {{ input.getTimeout() }};
-
-/**
- * Set error in response on timeout.
- */
-page.onResourceTimeout = function (e) {
-    response        = e;
-    response.status = e.errorCode;
-};
-
-/**
- * Set response from resource.
- */
-page.onResourceReceived = function (r) {
-    if(!response.status) response = r;
-};
-
-/**
- * Add page errors to logs.
- */
-page.onError = function (msg, trace) {
- 
-    var error = {
-        message: msg,
-        trace: []
-    };
-    
-    trace.forEach(function(t) {
-        error.trace.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : ''));
-    });
-    
-    logs.push(error);
-};
-
-/**
- * Global error handling.
- */
-phantom.onError = function(msg, trace) {
-  
-    var stack = [];
-    
-    trace.forEach(function(t) {
-        stack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : ''));
-    });
-
-    response.status  = 500;
-    response.content = msg;
-    response.console = stack;
-
-    system.stdout.write(JSON.stringify(response, undefined, 4));
-    phantom.exit(1);
-};
-
-/**
- * Open page.
- *
- * @param string url
- * @param string method
- * @param string parameters
- * @param callable callback
- */
-page.open ('{{ input.getUrl() }}', '{{ input.getMethod() }}', '{{ input.getBody() }}', function (status) {
-
-    if(!delay) {
-        return procedure.execute(status);
-    }
-    
-    debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Delaying page render for ' + delay + ' second(s)');
-    
-    window.setTimeout(function () { 
-    
-        debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Rendering page after delaying for ' + delay + ' second(s)');
-        procedure.execute(status); 
-    
-    }, (delay * 1000));
-});
-
-/**
- * Command to execute on page load.
- */
-procedure.execute = function (status) {
-
-    if (status === 'success') {
-        
-        try {
-        
-            page.render('{{ input.getCaptureFile() }}');
-            
-            response.content = page.evaluate(function () {
-                return document.getElementsByTagName('html')[0].innerHTML
-            });
-        
-        } catch(e) {
-    
-            response.status  = 500;
-            response.content = e.message;
-        }
-    }
-    
-    response.console = logs;
-    
-    system.stderr.write(debug.join('\\n') + '\\n');
-    system.stdout.write(JSON.stringify(response, undefined, 4));
-    
-    phantom.exit();
-};
-
-{% endautoescape %}

+ 35 - 94
src/JonnyW/PhantomJs/Resources/procedures/http_default.proc

@@ -1,143 +1,84 @@
+
+[% autoescape false %]
 {% autoescape false %}
 
 /**
- * Set up page and script parameters.
+ * Set up page and script parameters
  */
 var page       = require('webpage').create(),
     system     = require('system'),
-    headers    = {{ input.getHeaders('json') }},
-    delay      = {{ input.getDelay() }},
-    vpWidth    = {{ input.getViewportWidth() }},
-    vpHeight   = {{ input.getViewportHeight() }},
     response   = {},
     debug      = [],
     logs       = [],
     procedure  = {};
 
 /**
- * Set viewport size.
+ * Global variables
+ */
+[[ engine.load('global_variables') ]]
+
+/**
+ * Define width & height of screenshot
  */
-if(vpWidth && vpHeight) {
-    
-    debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Set viewport size ~ width: ' + vpWidth + ' height: ' + vpHeight);
-    
-    page.viewportSize = {
-        width: vpWidth,
-        height: vpHeight
-    };
-}
+[[ engine.load('page_clip_rect') ]]
+
+/**
+ * Define viewport size.
+ */
+[[ engine.load('page_viewport_size') ]]
+
 
 /**
  * Define custom headers.
  */
-page.customHeaders = headers ? headers : {};
+[[ engine.load('page_custom_headers') ]]
 
 /**
- * Set timeout.
+ * Page settings
  */
-page.settings.resourceTimeout = {{ input.getTimeout() }};
+[[ engine.load('page_settings') ]]
 
 /**
- * Set error in response on timeout.
+ * On resource timeout
  */
-page.onResourceTimeout = function (e) {
-    response        = e;
-    response.status = e.errorCode;
+page.onResourceTimeout = function (error) {
+    [[ engine.load('page_on_resource_timeout') ]]
 };
 
 /**
- * Set response from resource.
+ * On resource received
  */
-page.onResourceReceived = function (r) {
-    if(!response.status) response = r;
+page.onResourceReceived = function (resource) {
+    [[ engine.load('page_on_resource_received') ]]
 };
 
 /**
- * Add page errors to logs.
+ * Handle page errors
  */
 page.onError = function (msg, trace) {
- 
-    var error = {
-        message: msg,
-        trace: []
-    };
-    
-    trace.forEach(function(t) {
-        error.trace.push((t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : ''));
-    });
-    
-    logs.push(error);
+    [[ engine.load('page_on_error') ]]
 };
 
 /**
- * Global error handling.
+ * Handle global errors
  */
 phantom.onError = function(msg, trace) {
-  
-    var stack = [];
-    
-    trace.forEach(function(t) {
-        stack.push((t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : ''));
-    });
-
-    response.status  = 500;
-    response.content = msg;
-    response.console = stack;
-
-    system.stdout.write(JSON.stringify(response, undefined, 4));
-    phantom.exit(1);
+    [[ engine.load('phantom_on_error') ]]
 };
 
 /**
- * Open page.
- *
- * @param string url
- * @param string method
- * @param string parameters
- * @param callable callback
+ * Open page
  */
 page.open ('{{ input.getUrl() }}', '{{ input.getMethod() }}', '{{ input.getBody() }}', function (status) {
-    
-    if(!delay) {
-        return procedure.execute(status);
-    }
-    
-    debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Delaying page render for ' + delay + ' second(s)');
-    
-    window.setTimeout(function () { 
-    
-        debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Rendering page after delaying for ' + delay + ' second(s)');
-        procedure.execute(status); 
-        
-    }, (delay * 1000));
+    [[ engine.load('page_open') ]]
 });
 
 /**
- * Command to execute on page load.
+ * Execute procedure
  */
 procedure.execute = function (status) {
-
-    if (status === 'success') {
-        
-        try {
-        
-            response.content = page.evaluate(function () {
-                return document.getElementsByTagName('html')[0].innerHTML
-            });
-        
-        } catch(e) {
-    
-            response.status  = 500;
-            response.content = e.message;
-        }
-    }
-    
-    response.console = logs;
-    
-    system.stderr.write(debug.join('\\n') + '\\n');
-    system.stdout.write(JSON.stringify(response, undefined, 4));
-    
-    phantom.exit();
+    [[ engine.load( 'procedure_' ~ procedure_type ) ]]
 };
 
-{% endautoescape %}
+{% endautoescape %}
+[% endautoescape %]

+ 21 - 0
src/JonnyW/PhantomJs/Resources/procedures/page_clip_rect.partial

@@ -0,0 +1,21 @@
+
+{% if input.getType() == 'capture' %}
+
+var top    = {{ input.getRectTop() }},
+    left   = {{ input.getRectLeft() }},
+    width  = {{ input.getRectWidth() }},
+    height = {{ input.getRectHeight() }};
+
+if(width && height) {
+    
+    debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Set capture clipping size ~ top: ' + top + ' left: ' + left + ' ' + width + 'x' + height);
+    
+    page.clipRect = {
+        top: top,
+        left: left,
+        width: width,
+        height: height
+    };
+}
+
+{% endif %}

+ 5 - 0
src/JonnyW/PhantomJs/Resources/procedures/page_custom_headers.partial

@@ -0,0 +1,5 @@
+
+var headers = {{ input.getHeaders('json') }};
+
+page.customHeaders = headers ? headers : {};
+

+ 12 - 0
src/JonnyW/PhantomJs/Resources/procedures/page_on_error.partial

@@ -0,0 +1,12 @@
+
+var error = {
+    message: msg,
+    trace: []
+};
+
+trace.forEach(function(t) {
+    error.trace.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : ''));
+});
+
+logs.push(error);
+

+ 5 - 0
src/JonnyW/PhantomJs/Resources/procedures/page_on_resource_received.partial

@@ -0,0 +1,5 @@
+
+if(!response.status) {
+    response = resource;
+}
+

+ 4 - 0
src/JonnyW/PhantomJs/Resources/procedures/page_on_resource_timeout.partial

@@ -0,0 +1,4 @@
+
+response        = error;
+response.status = error.errorCode;
+

+ 16 - 0
src/JonnyW/PhantomJs/Resources/procedures/page_open.partial

@@ -0,0 +1,16 @@
+
+var delay = {{ input.getDelay() }};
+
+if(!delay) {
+    return procedure.execute(status);
+}
+
+debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Delaying page render for ' + delay + ' second(s)');
+
+window.setTimeout(function () { 
+
+    debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Rendering page after delaying for ' + delay + ' second(s)');
+    procedure.execute(status); 
+
+}, (delay * 1000));
+

+ 3 - 0
src/JonnyW/PhantomJs/Resources/procedures/page_settings.partial

@@ -0,0 +1,3 @@
+
+page.settings.resourceTimeout = {{ input.getTimeout() }};
+

+ 14 - 0
src/JonnyW/PhantomJs/Resources/procedures/page_viewport_size.partial

@@ -0,0 +1,14 @@
+
+var width  = {{ input.getViewportWidth() }},
+    height = {{ input.getViewportHeight() }};
+
+if(width && height) {
+    
+    debug.push(new Date().toISOString().slice(0, -5) + ' [INFO] PhantomJS - Set viewport size ~ width: ' + width + ' height: ' + height);
+    
+    page.viewportSize = {
+        width: width,
+        height: height
+    };
+}
+

+ 14 - 0
src/JonnyW/PhantomJs/Resources/procedures/phantom_on_error.partial

@@ -0,0 +1,14 @@
+    
+var stack = [];
+
+trace.forEach(function(t) {
+    stack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : ''));
+});
+
+response.status  = 500;
+response.content = msg;
+response.console = stack;
+
+system.stdout.write(JSON.stringify(response, undefined, 4));
+phantom.exit(1);
+

+ 25 - 0
src/JonnyW/PhantomJs/Resources/procedures/procedure_capture.partial

@@ -0,0 +1,25 @@
+
+if (status === 'success') {
+    
+    try {
+    
+        page.render('{{ input.getCaptureFile() }}');
+        
+        response.content = page.evaluate(function () {
+            return document.getElementsByTagName('html')[0].innerHTML
+        });
+    
+    } catch(e) {
+
+        response.status  = 500;
+        response.content = e.message;
+    }
+}
+
+response.console = logs;
+
+system.stderr.write(debug.join('\\n') + '\\n');
+system.stdout.write(JSON.stringify(response, undefined, 4));
+
+phantom.exit();
+

+ 23 - 0
src/JonnyW/PhantomJs/Resources/procedures/procedure_default.partial

@@ -0,0 +1,23 @@
+
+if (status === 'success') {
+    
+    try {
+    
+        response.content = page.evaluate(function () {
+            return document.getElementsByTagName('html')[0].innerHTML
+        });
+    
+    } catch(e) {
+
+        response.status  = 500;
+        response.content = e.message;
+    }
+}
+
+response.console = logs;
+
+system.stderr.write(debug.join('\\n') + '\\n');
+system.stdout.write(JSON.stringify(response, undefined, 4));
+
+phantom.exit();
+

+ 1 - 1
src/JonnyW/PhantomJs/Resources/procedures/validator.proc

@@ -24,6 +24,6 @@ system.stdout.write(
   JSON.stringify(syntax, undefined, 4)
 );
 
-phantom.exit(1);
+phantom.exit();
 
 {% endautoescape %}

+ 14 - 6
src/JonnyW/PhantomJs/Tests/Integration/ClientTest.php

@@ -10,6 +10,7 @@ namespace JonnyW\PhantomJs\Tests\Integration;
 
 use JonnyW\PhantomJs\Test\TestCase;
 use JonnyW\PhantomJs\Client;
+use JonnyW\PhantomJs\DependencyInjection\ServiceContainer;
 
 /**
  * PHP PhantomJs
@@ -60,13 +61,12 @@ EOF;
         $procedureLoader        = $procedureLoaderFactory->createProcedureLoader($this->directory);
 
         $client = $this->getClient();
+        $client->setProcedure('test');
         $client->getProcedureLoader()->addLoader($procedureLoader);
 
         $request  = $client->getMessageFactory()->createRequest();
         $response = $client->getMessageFactory()->createResponse();
 
-        $request->setType('test');
-
         $client->send($request, $response);
 
         $this->assertSame($content, $response->getContent());
@@ -95,13 +95,12 @@ EOF;
         $procedureLoader        = $procedureLoaderFactory->createProcedureLoader($this->directory);
 
         $client = $this->getClient();
+        $client->setProcedure('test');
         $client->getProcedureLoader()->addLoader($procedureLoader);
 
         $request  = $client->getMessageFactory()->createRequest();
         $response = $client->getMessageFactory()->createResponse();
 
-        $request->setType('test');
-
         $client->send($request, $response);
     }
 
@@ -584,7 +583,7 @@ EOF;
     public function testDebugLogsDebugInfoToClientLog()
     {
         $client = $this->getClient();
-        $client->debug(true);
+        $client->getEngine()->debug(true);
 
         $request  = $client->getMessageFactory()->createRequest();
         $response = $client->getMessageFactory()->createResponse();
@@ -608,7 +607,16 @@ EOF;
      */
     protected function getClient()
     {
-        return Client::getInstance();
+        $serviceContainer = ServiceContainer::getInstance();
+
+        $client = new Client(
+            $serviceContainer->get('engine'),
+            $serviceContainer->get('procedure_loader'),
+            $serviceContainer->get('procedure_compiler'),
+            $serviceContainer->get('message_factory')
+        );
+
+        return $client;
     }
 
 /** +++++++++++++++++++++++++++++++++++ **/

+ 265 - 0
src/JonnyW/PhantomJs/Tests/Integration/Procedure/ProcedureCompilerTest.php

@@ -0,0 +1,265 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Integration\Procedure;
+
+use JonnyW\PhantomJs\Http\Request;
+use JonnyW\PhantomJs\Procedure\ProcedureCompiler;
+use JonnyW\PhantomJs\DependencyInjection\ServiceContainer;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class ProcedureCompilerTest extends \PHPUnit_Framework_TestCase
+{
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++++ TESTS ++++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Test can compile procedure
+     *
+     * @access public
+     * @return void
+     */
+    public function testCanCompileProcedure()
+    {
+        $procedure = $this->getProcedure('http_default');
+
+        $uncompiled = $procedure->getTemplate();
+
+        $request = $this->getRequest();
+        $request->setUrl('http://test.com');
+
+        $compiler = $this->getProcedureCompiler();
+        $compiler->compile($procedure, $request);
+
+        $this->assertNotSame($uncompiled, $procedure->getTemplate());
+    }
+
+    /**
+     * Test procedure is loaded from cache
+     * if cache is enabled.
+     *
+     * @access public
+     * @return void
+     */
+    public function testProcedureIsLoadedFromCacheIfCacheIsEnabled()
+    {
+        $procedure1 = $this->getProcedure('http_default');
+        $procedure2 = $this->getProcedure('http_default');
+
+        $request = $this->getRequest();
+        $request->setUrl('http://test.com');
+
+        $renderer = $this->getMock('\JonnyW\PhantomJs\Template\TemplateRendererInterface');
+        $renderer->expects($this->exactly(1))
+            ->method('render')
+            ->will($this->returnValue('var test=1; phantom.exit(1);'));
+
+        $serviceContainer = ServiceContainer::getInstance();
+
+        $compiler = new ProcedureCompiler(
+            $serviceContainer->get('phantomjs.procedure.chain_loader'),
+            $serviceContainer->get('phantomjs.procedure.procedure_validator'),
+            $serviceContainer->get('phantomjs.cache.file_cache'),
+            $renderer
+        );
+
+        $compiler->enableCache();
+        $compiler->compile($procedure1, $request);
+        $compiler->compile($procedure2, $request);
+    }
+
+    /**
+     * Test procedure is not loaded from
+     * cache if cache is disabled.
+     *
+     * @access public
+     * @return void
+     */
+    public function testProcedureIsNotLoadedFromCacheIfCacheIsDisabled()
+    {
+        $procedure1 = $this->getProcedure('http_default');
+        $procedure2 = $this->getProcedure('http_default');
+
+        $request = $this->getRequest();
+        $request->setUrl('http://test.com');
+
+        $renderer = $this->getMock('\JonnyW\PhantomJs\Template\TemplateRendererInterface');
+        $renderer->expects($this->exactly(2))
+            ->method('render')
+            ->will($this->returnValue('var test=1; phantom.exit(1);'));
+
+        $serviceContainer = ServiceContainer::getInstance();
+
+        $compiler = new ProcedureCompiler(
+            $serviceContainer->get('phantomjs.procedure.chain_loader'),
+            $serviceContainer->get('phantomjs.procedure.procedure_validator'),
+            $serviceContainer->get('phantomjs.cache.file_cache'),
+            $renderer
+        );
+
+        $compiler->disableCache();
+        $compiler->compile($procedure1, $request);
+        $compiler->compile($procedure2, $request);
+    }
+
+    /**
+     * Test procedure cache can be cleared.
+     *
+     * @access public
+     * @return void
+     */
+    public function testProcedureCacheCanBeCleared()
+    {
+        $procedure1 = $this->getProcedure('http_default');
+        $procedure2 = $this->getProcedure('http_default');
+
+        $request = $this->getRequest();
+        $request->setUrl('http://test.com');
+
+        $renderer = $this->getMock('\JonnyW\PhantomJs\Template\TemplateRendererInterface');
+        $renderer->expects($this->exactly(2))
+            ->method('render')
+            ->will($this->returnValue('var test=1; phantom.exit(1);'));
+
+        $serviceContainer = ServiceContainer::getInstance();
+
+        $compiler = new ProcedureCompiler(
+            $serviceContainer->get('phantomjs.procedure.chain_loader'),
+            $serviceContainer->get('phantomjs.procedure.procedure_validator'),
+            $serviceContainer->get('phantomjs.cache.file_cache'),
+            $renderer
+        );
+
+        $compiler->compile($procedure1, $request);
+        $compiler->clearCache();
+        $compiler->compile($procedure2, $request);
+    }
+
+    /**
+     * Test syntax exception is thrown if compiled
+     * template is not valid.
+     *
+     * @access public
+     * @return void
+     */
+    public function testSyntaxExceptionIsThrownIfCompiledTemplateIsNotValid()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\SyntaxException');
+
+        $template = <<<EOF
+    console.log(;
+EOF;
+        $procedure = $this->getProcedure('http_default');
+        $procedure->setTemplate($template);
+
+        $request = $this->getRequest();
+        $request->setUrl('http://test.com');
+
+        $compiler = $this->getProcedureCompiler();
+        $compiler->compile($procedure, $request);
+
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get procedure compiler.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureCompiler
+     */
+    protected function getProcedureCompiler()
+    {
+        $serviceContainer = ServiceContainer::getInstance();
+
+        $compiler = new ProcedureCompiler(
+            $serviceContainer->get('phantomjs.procedure.chain_loader'),
+            $serviceContainer->get('phantomjs.procedure.procedure_validator'),
+            $serviceContainer->get('phantomjs.cache.file_cache'),
+            $serviceContainer->get('phantomjs.procedure.template_renderer')
+        );
+
+        return $compiler;
+    }
+
+    /**
+     * getProcedure function.
+     *
+     * @access protected
+     * @param  string                                         $id
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureInterface
+     */
+    protected function getProcedure($id)
+    {
+        return ServiceContainer::getInstance()->get('procedure_loader')->load($id);
+    }
+
+    /**
+     * Get request
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Http\Request
+     */
+    protected function getRequest()
+    {
+        $request = new Request();
+
+        return $request;
+    }
+
+/** +++++++++++++++++++++++++++++++ **/
+/** ++++++++++ UTILITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Set up tasks.
+     *
+     * @access protected
+     * @return void
+     */
+    protected function setUp()
+    {
+        parent::setUp();
+
+        $this->cleanup();
+    }
+
+    /**
+     * Tear down tasks.
+     *
+     * @access protected
+     * @return void
+     */
+    protected function tearDown()
+    {
+        parent::setUp();
+
+        $this->cleanup();
+    }
+
+    /**
+     * Clean up cache files.
+     *
+     * @access protected
+     * @return void
+     */
+    protected function cleanup()
+    {
+        $cache = sprintf('%s/phantomjs_*', sys_get_temp_dir());
+
+        array_map('unlink', glob($cache));
+    }
+}

+ 5 - 130
src/JonnyW/PhantomJs/Tests/Integration/Procedure/ProcedureValidatorTest.php

@@ -8,14 +8,8 @@
  */
 namespace JonnyW\PhantomJs\Tests\Integration\Procedure;
 
-use Twig_Environment;
-use Twig_Loader_String;
 use Symfony\Component\Config\FileLocator;
 use JonnyW\PhantomJs\Client;
-use JonnyW\PhantomJs\Cache\FileCache;
-use JonnyW\PhantomJs\Parser\JsonParser;
-use JonnyW\PhantomJs\Template\TemplateRenderer;
-use JonnyW\PhantomJs\Procedure\Input;
 use JonnyW\PhantomJs\Procedure\Procedure;
 use JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface;
 use JonnyW\PhantomJs\Procedure\ProcedureValidator;
@@ -49,14 +43,8 @@ class ProcedureValidatorTest extends \PHPUnit_Framework_TestCase
         $procedureLoader = $this->getProcedureLoader();
         $esprima         = $this->getEsprima();
 
-        $client    = $this->getClient();
-        $procedure = $this->getProcedure();
-        $message   = $this->getInput();
-
-        $procedure->setTemplate('return false; var');
-
         $validator = $this->getValidator($procedureLoader, $esprima);
-        $validator->validate($client, $procedure, $message);
+        $validator->validate('return false; var');
     }
 
     /**
@@ -70,16 +58,10 @@ class ProcedureValidatorTest extends \PHPUnit_Framework_TestCase
         $procedureLoader = $this->getProcedureLoader();
         $esprima         = $this->getEsprima();
 
-        $client    = $this->getClient();
-        $procedure = $this->getProcedure();
-        $message   = $this->getInput();
-
-        $procedure->setTemplate('return false; var');
-
         try {
 
             $validator = $this->getValidator($procedureLoader, $esprima);
-            $validator->validate($client, $procedure, $message);
+            $validator->validate('return false; var');
 
         } catch (\JonnyW\PhantomJs\Exception\SyntaxException $e) {
             $this->assertNotEmpty($e->getErrors());
@@ -101,14 +83,8 @@ class ProcedureValidatorTest extends \PHPUnit_Framework_TestCase
         $procedureLoader = $this->getProcedureLoader();
         $esprima         = $this->getEsprima();
 
-        $client    = $this->getClient();
-        $procedure = $this->getProcedure();
-        $message   = $this->getInput();
-
-        $procedure->setTemplate('var test = function () { console.log("ok"); }');
-
         $validator = $this->getValidator($procedureLoader, $esprima);
-        $validator->validate($client, $procedure, $message);
+        $validator->validate('var test = function () { console.log("ok"); }');
     }
 
     /**
@@ -122,15 +98,9 @@ class ProcedureValidatorTest extends \PHPUnit_Framework_TestCase
         $procedureLoader = $this->getProcedureLoader();
         $esprima         = $this->getEsprima();
 
-        $client    = $this->getClient();
-        $procedure = $this->getProcedure();
-        $message   = $this->getInput();
-
-        $procedure->setTemplate('var test = function () { console.log("ok"); }; phantom.exit(1);');
-
         $validator = $this->getValidator($procedureLoader, $esprima);
 
-        $this->assertTrue($validator->validate($client, $procedure, $message));
+        $this->assertTrue($validator->validate('var test = function () { console.log("ok"); }; phantom.exit(1);'));
     }
 
     /**
@@ -145,15 +115,9 @@ class ProcedureValidatorTest extends \PHPUnit_Framework_TestCase
         $procedureLoader = $this->getProcedureLoader();
         $esprima         = $this->getEsprima();
 
-        $client    = $this->getClient();
-        $procedure = $this->getProcedure();
-        $message   = $this->getInput();
-
-        $procedure->setTemplate('/** * Test comment **/ var test = function () { console.log("ok"); }; phantom.exit(1);');
-
         $validator = $this->getValidator($procedureLoader, $esprima);
 
-        $this->assertTrue($validator->validate($client, $procedure, $message));
+        $this->assertTrue($validator->validate('/** * Test comment **/ var test = function () { console.log("ok"); }; phantom.exit(1);'));
     }
 
 /** +++++++++++++++++++++++++++++++++++ **/
@@ -175,20 +139,6 @@ class ProcedureValidatorTest extends \PHPUnit_Framework_TestCase
         return $validator;
     }
 
-    /**
-     * Get client.
-     *
-     * @access protected
-     * @return \JonnyW\PhantomJs\Client
-     */
-    protected function getClient()
-    {
-        $client = Client::getInstance();
-        $client->setPhantomJs(sprintf('%s/../../../../../../bin/phantomjs', __DIR__));
-
-        return $client;
-    }
-
     /**
      * Get procedure loader.
      *
@@ -200,81 +150,6 @@ class ProcedureValidatorTest extends \PHPUnit_Framework_TestCase
         return  Client::getInstance()->getProcedureLoader();
     }
 
-    /**
-     * Get procedure instance.
-     *
-     * @access protected
-     * @return \JonnyW\PhantomJs\Procedure\Procedure
-     */
-    protected function getProcedure()
-    {
-        $procedure = new Procedure(
-            $this->getParser(),
-            $this->getCache(),
-            $this->getRenderer()
-        );
-
-        return $procedure;
-    }
-
-    /**
-     * Get parser.
-     *
-     * @access protected
-     * @return \JonnyW\PhantomJs\Parser\JsonParser
-     */
-    protected function getParser()
-    {
-        $parser = new JsonParser();
-
-        return $parser;
-    }
-
-    /**
-     * Get cache.
-     *
-     * @access protected
-     * @param  string                            $cacheDir  (default: '')
-     * @param  string                            $extension (default: 'proc')
-     * @return \JonnyW\PhantomJs\Cache\FileCache
-     */
-    protected function getCache($cacheDir = '', $extension = 'proc')
-    {
-        $cache = new FileCache(($cacheDir ? $cacheDir : sys_get_temp_dir()), 'proc');
-
-        return $cache;
-    }
-
-    /**
-     * Get template renderer.
-     *
-     * @access protected
-     * @return \JonnyW\PhantomJs\Template\TemplateRenderer
-     */
-    protected function getRenderer()
-    {
-        $twig = new Twig_Environment(
-            new Twig_Loader_String()
-        );
-
-        $renderer = new TemplateRenderer($twig);
-
-        return $renderer;
-    }
-
-    /**
-     * Get input
-     *
-     * @access protected
-     * @return \JonnyW\PhantomJs\Procedure\Input
-     */
-    protected function getInput()
-    {
-        $input = new Input();
-
-        return $input;
-    }
-
     /**
      * Get esprima.
      *

+ 21 - 0
src/JonnyW/PhantomJs/Tests/Unit/Cache/FileCacheTest.php

@@ -198,6 +198,27 @@ class FileCacheTest extends \PHPUnit_Framework_TestCase
         $this->assertFileNotExists($file);
     }
 
+    /**
+     * Test data can be deleted from
+     * cache using wildcard.
+     *.
+     *
+     * @access public
+     * @return void
+     */
+    public function testDataCanBeDeletedFromCacheUsingWildcard()
+    {
+        $fileCache = $this->getFileCache($this->directory, 'txt');
+
+        $file1 = $fileCache->save('test_file_1', 'Test1');
+        $file2 = $fileCache->save('test_file_2', 'Test2');
+
+        $fileCache->delete('test_file_*');
+
+        $this->assertFileNotExists($file1);
+        $this->assertFileNotExists($file2);
+    }
+
 /** +++++++++++++++++++++++++++++++++++ **/
 /** ++++++++++ TEST ENTITIES ++++++++++ **/
 /** +++++++++++++++++++++++++++++++++++ **/

+ 43 - 244
src/JonnyW/PhantomJs/Tests/Unit/ClientTest.php

@@ -9,9 +9,10 @@
 namespace JonnyW\PhantomJs\Tests\Unit;
 
 use JonnyW\PhantomJs\Client;
+use JonnyW\PhantomJs\Engine;
 use JonnyW\PhantomJs\Http\MessageFactoryInterface;
 use JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface;
-use JonnyW\PhantomJs\Procedure\ProcedureValidatorInterface;
+use JonnyW\PhantomJs\Procedure\ProcedureCompilerInterface;
 
 /**
  * PHP PhantomJs
@@ -38,270 +39,54 @@ class ClientTest extends \PHPUnit_Framework_TestCase
     }
 
     /**
-     * Test can get message factory
-     *
-     * @return void
-     */
-    public function testCanGetMessageFactory()
-    {
-        $procedureLoader    = $this->getProcedureLoader();
-        $procedureValidator = $this->getProcedureValidator();
-        $messageFactory     = $this->getMessageFactory();
-
-        $client = $this->getClient($procedureLoader, $procedureValidator, $messageFactory);
-
-        $this->assertInstanceOf('\JonnyW\PhantomJs\Http\MessageFactoryInterface', $client->getMessageFactory());
-    }
-
-    /**
-     * Test can get procedure loader.
-     *
-     * @return void
-     */
-    public function testCanGetProcedureLoader()
-    {
-        $procedureLoader    = $this->getProcedureLoader();
-        $procedureValidator = $this->getProcedureValidator();
-        $messageFactory     = $this->getMessageFactory();
-
-        $client = $this->getClient($procedureLoader, $procedureValidator, $messageFactory);
-
-        $this->assertInstanceOf('\JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface', $client->getProcedureLoader());
-    }
-
-    /**
-     * Test invalid executable exception is thrown
-     * if phantom JS path is invalid.
-     *
-     * @access public
-     * @return void
-     */
-    public function testInvalidExecutableExceptionIsThrownIfPhantomJSPathIsInvalid()
-    {
-        $this->setExpectedException('\JonnyW\PhantomJs\Exception\InvalidExecutableException');
-
-        $procedureLoader    = $this->getProcedureLoader();
-        $procedureValidator = $this->getProcedureValidator();
-        $messageFactory     = $this->getMessageFactory();
-
-        $client = $this->getClient($procedureLoader, $procedureValidator, $messageFactory);
-        $client->setPhantomJs('/invalid/phantomjs/path');
-    }
-
-    /**
-     * Test default phantom JS path is returned
-     * if no custom path is set.
-     *
-     * @access public
-     * @return void
-     */
-    public function testDefaultPhantomJSPathIsReturnedIfNoCustomPathIsSet()
-    {
-        $procedureLoader    = $this->getProcedureLoader();
-        $procedureValidator = $this->getProcedureValidator();
-        $messageFactory     = $this->getMessageFactory();
-
-        $client = $this->getClient($procedureLoader, $procedureValidator, $messageFactory);
-
-        $this->assertSame('bin/phantomjs', $client->getPhantomJs());
-    }
-
-    /**
-     * Test can log data.
-     *
-     * @access public
-     * @return void
-     */
-    public function testCanLogData()
-    {
-        $procedureLoader    = $this->getProcedureLoader();
-        $procedureValidator = $this->getProcedureValidator();
-        $messageFactory     = $this->getMessageFactory();
-
-        $log = 'Test log info';
-
-        $client = $this->getClient($procedureLoader, $procedureValidator, $messageFactory);
-        $client->log($log);
-
-        $this->assertSame($log, $client->getLog());
-    }
-
-    /**
-     * Test can clear log.
-     *
-     * @access public
-     * @return void
-     */
-    public function testCanClearLog()
-    {
-        $procedureLoader    = $this->getProcedureLoader();
-        $procedureValidator = $this->getProcedureValidator();
-        $messageFactory     = $this->getMessageFactory();
-
-        $log = 'Test log info';
-
-        $client = $this->getClient($procedureLoader, $procedureValidator, $messageFactory);
-        $client->log($log);
-        $client->clearLog();
-
-        $this->assertEmpty($client->getLog());
-    }
-
-    /**
-     * Test can add run option.
-     *
-     * @access public
-     * @return void
-     */
-    public function testCanAddRunOption()
-    {
-        $procedureLoader    = $this->getProcedureLoader();
-        $procedureValidator = $this->getProcedureValidator();
-        $messageFactory     = $this->getMessageFactory();
-
-        $options = array(
-            'option1',
-            'option2'
-        );
-
-        $client = $this->getClient($procedureLoader, $procedureValidator, $messageFactory);
-        $client->setOptions($options);
-        $client->addOption('option3');
-
-        array_push($options, 'option3');
-
-        $this->assertSame($options, $client->getOptions());
-    }
-
-    /**
-     * Test invalid executable exception is thrown when
-     * building command if path to phantom JS is valid.
-     *
-     * @access public
-     * @return void
-     */
-    public function testInvalidExecutableExceptionIsThrownWhenBuildingCommandIfPathToPhantomJSIsInvalid()
-    {
-        $this->setExpectedException('\JonnyW\PhantomJs\Exception\InvalidExecutableException');
-
-        $procedureLoader    = $this->getProcedureLoader();
-        $procedureValidator = $this->getProcedureValidator();
-        $messageFactory     = $this->getMessageFactory();
-
-        $client = $this->getClient($procedureLoader, $procedureValidator, $messageFactory);
-
-        $phantomJs = new \ReflectionProperty(get_class($client), 'phantomJs');
-        $phantomJs->setAccessible(true);
-        $phantomJs->setValue($client, 'invalid/path');
-
-        $client->getCommand();
-    }
-
-    /**
-     * Test command contains phantom JS executable
-     *
-     * @access public
-     * @return void
-     */
-    public function testCommandContainsPhantomJSExecutable()
-    {
-        $procedureLoader    = $this->getProcedureLoader();
-        $procedureValidator = $this->getProcedureValidator();
-        $messageFactory     = $this->getMessageFactory();
-
-        $client = $this->getClient($procedureLoader, $procedureValidator, $messageFactory);
-
-        $this->assertContains($client->getPhantomJs(), $client->getCommand());
-    }
-
-    /**
-     * Test debug flag can be set.
+     * Test can get engine.
      *
-     * @access public
      * @return void
      */
-    public function testDebugFlagCanBeSet()
+    public function testCanGetEngne()
     {
+        $engine             = $this->getEngine();
         $procedureLoader    = $this->getProcedureLoader();
-        $procedureValidator = $this->getProcedureValidator();
+        $procedureCompiler  = $this->getProcedureCompiler();
         $messageFactory     = $this->getMessageFactory();
 
-        $client = $this->getClient($procedureLoader, $procedureValidator, $messageFactory);
-        $client->debug(true);
+        $client = $this->getClient($engine, $procedureLoader, $procedureCompiler, $messageFactory);
 
-        $this->assertContains('--debug=true', $client->getCommand());
+        $this->assertInstanceOf('\JonnyW\PhantomJs\Engine', $client->getEngine());
     }
 
     /**
-     * Test debug flag is not set if
-     * debugging is not enabled.
-     *
-     * @access public
-     * @return void
-     */
-    public function testDebugFlagIsNotSetIfDebuggingIsNotEnabled()
-    {
-        $procedureLoader    = $this->getProcedureLoader();
-        $procedureValidator = $this->getProcedureValidator();
-        $messageFactory     = $this->getMessageFactory();
-
-        $client = $this->getClient($procedureLoader, $procedureValidator, $messageFactory);
-        $client->debug(false);
-
-        $this->assertNotContains('--debug=true', $client->getCommand());
-    }
-
-    /**
-     * Test command contains run options.
+     * Test can get message factory
      *
-     * @access public
      * @return void
      */
-    public function testCommandContainsRunOptions()
+    public function testCanGetMessageFactory()
     {
+        $engine             = $this->getEngine();
         $procedureLoader    = $this->getProcedureLoader();
-        $procedureValidator = $this->getProcedureValidator();
+        $procedureCompiler  = $this->getProcedureCompiler();
         $messageFactory     = $this->getMessageFactory();
 
-        $option1 = '--local-storage-path=/some/path';
-        $option2 = '--local-storage-quota=5';
-        $option3 = '--local-to-remote-url-access=true';
-
-        $client = $this->getClient($procedureLoader, $procedureValidator, $messageFactory);
-        $client->addOption($option1);
-        $client->addOption($option2);
-        $client->addOption($option3);
-
-        $command = $client->getCommand();
+        $client = $this->getClient($engine, $procedureLoader, $procedureCompiler, $messageFactory);
 
-        $this->assertContains($option1, $command);
-        $this->assertContains($option2, $command);
-        $this->assertContains($option3, $command);
+        $this->assertInstanceOf('\JonnyW\PhantomJs\Http\MessageFactoryInterface', $client->getMessageFactory());
     }
 
     /**
-     * Test debug flag is set if runs options
-     * are also set.
+     * Test can get procedure loader.
      *
-     * @access public
      * @return void
      */
-    public function testDebugFlagIsSetIfRunOptionsAreAlsoSet()
+    public function testCanGetProcedureLoader()
     {
+        $engine             = $this->getEngine();
         $procedureLoader    = $this->getProcedureLoader();
-        $procedureValidator = $this->getProcedureValidator();
+        $procedureCompiler  = $this->getProcedureCompiler();
         $messageFactory     = $this->getMessageFactory();
 
-        $option = '--local-storage-path=/some/path';
-
-        $client = $this->getClient($procedureLoader, $procedureValidator, $messageFactory);
-        $client->addOption($option);
-        $client->debug(true);
+        $client = $this->getClient($engine, $procedureLoader, $procedureCompiler, $messageFactory);
 
-        $command = $client->getCommand();
-
-        $this->assertContains($option, $command);
-        $this->assertContains('--debug=true', $command);
+        $this->assertInstanceOf('\JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface', $client->getProcedureLoader());
     }
 
 /** +++++++++++++++++++++++++++++++++++ **/
@@ -311,14 +96,15 @@ class ClientTest extends \PHPUnit_Framework_TestCase
     /**
      * Get client instance
      *
-     * @param  \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface    $procedureLoader
-     * @param  \JonnyW\PhantomJs\Procedure\ProcedureValidatorInterface $procedureValidator
-     * @param  \JonnyW\PhantomJs\Http\MessageFactoryInterface          $messageFactory
+     * @param  \JonnyW\PhantomJs\Engine                               $engine
+     * @param  \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface   $procedureLoader
+     * @param  \JonnyW\PhantomJs\Procedure\ProcedureCompilerInterface $procedureCompiler
+     * @param  \JonnyW\PhantomJs\Http\MessageFactoryInterface         $messageFactory
      * @return \JonnyW\PhantomJs\Client
      */
-    protected function getClient(ProcedureLoaderInterface $procedureLoader, ProcedureValidatorInterface $procedureValidator, MessageFactoryInterface $messageFactory)
+    protected function getClient(Engine $engine, ProcedureLoaderInterface $procedureLoader, ProcedureCompilerInterface $procedureCompiler, MessageFactoryInterface $messageFactory)
     {
-        $client = new Client($procedureLoader, $procedureValidator, $messageFactory);
+        $client = new Client($engine, $procedureLoader, $procedureCompiler, $messageFactory);
 
         return $client;
     }
@@ -327,6 +113,19 @@ class ClientTest extends \PHPUnit_Framework_TestCase
 /** ++++++++++ MOCKS / STUBS ++++++++++ **/
 /** +++++++++++++++++++++++++++++++++++ **/
 
+    /**
+     * Get engine
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Engine
+     */
+    protected function getEngine()
+    {
+        $engine = $this->getMock('\JonnyW\PhantomJs\Engine');
+
+        return $engine;
+    }
+
     /**
      * Get message factory
      *
@@ -357,12 +156,12 @@ class ClientTest extends \PHPUnit_Framework_TestCase
      * Get procedure validator.
      *
      * @access protected
-     * @return \JonnyW\PhantomJs\Procedure\ProcedureLoaderInterface
+     * @return \JonnyW\PhantomJs\Procedure\ProcedureCompilerInterface
      */
-    protected function getProcedureValidator()
+    protected function getProcedureCompiler()
     {
-        $procedureValidator = $this->getMock('\JonnyW\PhantomJs\Procedure\ProcedureValidatorInterface');
+        $procedureCompiler = $this->getMock('\JonnyW\PhantomJs\Procedure\ProcedureCompilerInterface');
 
-        return $procedureValidator;
+        return $procedureCompiler;
     }
 }

+ 231 - 0
src/JonnyW/PhantomJs/Tests/Unit/EngineTest.php

@@ -0,0 +1,231 @@
+<?php
+
+/*
+ * This file is part of the php-phantomjs.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace JonnyW\PhantomJs\Tests\Unit;
+
+use JonnyW\PhantomJs\Engine;
+
+/**
+ * PHP PhantomJs
+ *
+ * @author Jon Wenmoth <contact@jonnyw.me>
+ */
+class EngineTest extends \PHPUnit_Framework_TestCase
+{
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++++++ TESTS ++++++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Test invalid executable exception is thrown
+     * if phantom JS path is invalid.
+     *
+     * @access public
+     * @return void
+     */
+    public function testInvalidExecutableExceptionIsThrownIfPhantomJSPathIsInvalid()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\InvalidExecutableException');
+
+        $engine = $this->getEngine();
+        $engine->setPath('/invalid/phantomjs/path');
+    }
+
+    /**
+     * Test default phantom JS path is returned
+     * if no custom path is set.
+     *
+     * @access public
+     * @return void
+     */
+    public function testDefaultPhantomJSPathIsReturnedIfNoCustomPathIsSet()
+    {
+        $engine = $this->getEngine();
+
+        $this->assertSame('bin/phantomjs', $engine->getPath());
+    }
+
+    /**
+     * Test can log data.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCanLogData()
+    {
+        $log = 'Test log info';
+
+        $engine = $this->getEngine();
+        $engine->log($log);
+
+        $this->assertSame($log, $engine->getLog());
+    }
+
+    /**
+     * Test can clear log.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCanClearLog()
+    {
+        $log = 'Test log info';
+
+        $engine = $this->getEngine();
+        $engine->log($log);
+        $engine->clearLog();
+
+        $this->assertEmpty($engine->getLog());
+    }
+
+    /**
+     * Test can add run option.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCanAddRunOption()
+    {
+        $options = array(
+            'option1',
+            'option2'
+        );
+
+        $engine = $this->getEngine();
+        $engine->setOptions($options);
+        $engine->addOption('option3');
+
+        array_push($options, 'option3');
+
+        $this->assertSame($options, $engine->getOptions());
+    }
+
+    /**
+     * Test invalid executable exception is thrown when
+     * building command if path to phantom JS is valid.
+     *
+     * @access public
+     * @return void
+     */
+    public function testInvalidExecutableExceptionIsThrownWhenBuildingCommandIfPathToPhantomJSIsInvalid()
+    {
+        $this->setExpectedException('\JonnyW\PhantomJs\Exception\InvalidExecutableException');
+
+        $engine = $this->getEngine();
+
+        $phantomJs = new \ReflectionProperty(get_class($engine), 'path');
+        $phantomJs->setAccessible(true);
+        $phantomJs->setValue($engine, 'invalid/path');
+
+        $engine->getCommand();
+    }
+
+    /**
+     * Test command contains phantom JS executable
+     *
+     * @access public
+     * @return void
+     */
+    public function testCommandContainsPhantomJSExecutable()
+    {
+        $engine = $this->getEngine();
+
+        $this->assertContains($engine->getPath(), $engine->getCommand());
+    }
+
+    /**
+     * Test debug flag can be set.
+     *
+     * @access public
+     * @return void
+     */
+    public function testDebugFlagCanBeSet()
+    {
+        $engine = $this->getEngine();
+        $engine->debug(true);
+
+        $this->assertContains('--debug=true', $engine->getCommand());
+    }
+
+    /**
+     * Test debug flag is not set if
+     * debugging is not enabled.
+     *
+     * @access public
+     * @return void
+     */
+    public function testDebugFlagIsNotSetIfDebuggingIsNotEnabled()
+    {
+        $engine = $this->getEngine();
+        $engine->debug(false);
+
+        $this->assertNotContains('--debug=true', $engine->getCommand());
+    }
+
+    /**
+     * Test command contains run options.
+     *
+     * @access public
+     * @return void
+     */
+    public function testCommandContainsRunOptions()
+    {
+        $option1 = '--local-storage-path=/some/path';
+        $option2 = '--local-storage-quota=5';
+        $option3 = '--local-to-remote-url-access=true';
+
+        $engine = $this->getEngine();
+        $engine->addOption($option1);
+        $engine->addOption($option2);
+        $engine->addOption($option3);
+
+        $command = $engine->getCommand();
+
+        $this->assertContains($option1, $command);
+        $this->assertContains($option2, $command);
+        $this->assertContains($option3, $command);
+    }
+
+    /**
+     * Test debug flag is set if runs options
+     * are also set.
+     *
+     * @access public
+     * @return void
+     */
+    public function testDebugFlagIsSetIfRunOptionsAreAlsoSet()
+    {
+        $option = '--local-storage-path=/some/path';
+
+        $engine = $this->getEngine();
+        $engine->addOption($option);
+        $engine->debug(true);
+
+        $command = $engine->getCommand();
+
+        $this->assertContains($option, $command);
+        $this->assertContains('--debug=true', $command);
+    }
+
+/** +++++++++++++++++++++++++++++++++++ **/
+/** ++++++++++ TEST ENTITIES ++++++++++ **/
+/** +++++++++++++++++++++++++++++++++++ **/
+
+    /**
+     * Get client instance
+     *
+     * @return \JonnyW\PhantomJs\Engine
+     */
+    protected function getEngine()
+    {
+        $engine = new Engine();
+
+        return $engine;
+    }
+}

+ 41 - 0
src/JonnyW/PhantomJs/Tests/Unit/Procedure/ChainProcedureLoaderTest.php

@@ -82,6 +82,47 @@ class ChainProcedureLoaderTest extends \PHPUnit_Framework_TestCase
         $chainProcedureLoader->load('test');
     }
 
+    /**
+     * Test invalid argument exception is thrown if
+     * not valid loader can be found when loading template
+     *
+     * @access public
+     * @return void
+     */
+    public function testInvalidArgumentExceptionIsThrownIfNoValidLoaderCanBeFoundWhenLoadingTemplate()
+    {
+        $this->setExpectedException('\InvalidArgumentException');
+
+        $procedureLoaders = array();
+
+        $chainProcedureLoader = $this->getChainProcedureLoader($procedureLoaders);
+        $chainProcedureLoader->loadTemplate('test');
+    }
+
+    /**
+     * Test template is returned if
+     * procedure template is loaded.
+     *
+     * @access public
+     * @return void
+     */
+    public function testTemplateIsReturnedIfProcedureTemplateIsLoaded()
+    {
+        $template = 'Test template';
+
+        $procedureLoader = $this->getProcedureLoader();
+        $procedureLoader->method('loadTemplate')
+            ->will($this->returnValue($template));
+
+        $procedureLoaders = array(
+            $procedureLoader
+        );
+
+        $chainProcedureLoader = $this->getChainProcedureLoader($procedureLoaders);
+
+        $this->assertSame($template, $chainProcedureLoader->loadTemplate('test'));
+    }
+
 /** +++++++++++++++++++++++++++++++++++ **/
 /** ++++++++++ TEST ENTITIES ++++++++++ **/
 /** +++++++++++++++++++++++++++++++++++ **/

+ 19 - 3
src/JonnyW/PhantomJs/Tests/Unit/Procedure/ProcedureFactoryTest.php

@@ -10,6 +10,7 @@ namespace JonnyW\PhantomJs\Tests\Unit\Procedure;
 
 use Twig_Environment;
 use Twig_Loader_String;
+use JonnyW\PhantomJs\Engine;
 use JonnyW\PhantomJs\Cache\FileCache;
 use JonnyW\PhantomJs\Cache\CacheInterface;
 use JonnyW\PhantomJs\Parser\JsonParser;
@@ -39,11 +40,12 @@ class ProcedureFactoryTest extends \PHPUnit_Framework_TestCase
      */
     public function testFactoryCanCreateInstanceOfProcedure()
     {
+        $engine    = $this->getEngine();
         $parser    = $this->getParser();
         $cache     = $this->getCache();
         $renderer  = $this->getRenderer();
 
-        $procedureFactory = $this->getProcedureFactory($parser, $cache, $renderer);
+        $procedureFactory = $this->getProcedureFactory($engine, $parser, $cache, $renderer);
 
         $this->assertInstanceOf('\JonnyW\PhantomJs\Procedure\Procedure', $procedureFactory->createProcedure());
     }
@@ -56,18 +58,32 @@ class ProcedureFactoryTest extends \PHPUnit_Framework_TestCase
      * Get procedure factory instance.
      *
      * @access protected
+     * @param  \JonnyW\PhantomJs\Engine                             $engine
      * @param  \JonnyW\PhantomJs\Parser\ParserInterface             $parser
      * @param  \JonnyW\PhantomJs\Cache\CacheInterface               $cacheHandler
      * @param  \JonnyW\PhantomJs\Template\TemplateRendererInterface $renderer
      * @return \JonnyW\PhantomJs\Procedure\ProcedureFactory
      */
-    protected function getProcedureFactory(ParserInterface $parser, CacheInterface $cacheHandler, TemplateRendererInterface $renderer)
+    protected function getProcedureFactory(Engine $engine, ParserInterface $parser, CacheInterface $cacheHandler, TemplateRendererInterface $renderer)
     {
-        $procedureFactory = new ProcedureFactory($parser, $cacheHandler, $renderer);
+        $procedureFactory = new ProcedureFactory($engine, $parser, $cacheHandler, $renderer);
 
         return $procedureFactory;
     }
 
+    /**
+     * Get engine.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Engine
+     */
+    protected function getEngine()
+    {
+        $engine = new Engine();
+
+        return $engine;
+    }
+
     /**
      * Get parser.
      *

+ 38 - 1
src/JonnyW/PhantomJs/Tests/Unit/Procedure/ProcedureLoaderTest.php

@@ -11,6 +11,7 @@ namespace JonnyW\PhantomJs\Tests\Unit\Procedure;
 use Twig_Environment;
 use Twig_Loader_String;
 use Symfony\Component\Config\FileLocatorInterface;
+use JonnyW\PhantomJs\Engine;
 use JonnyW\PhantomJs\Cache\FileCache;
 use JonnyW\PhantomJs\Cache\CacheInterface;
 use JonnyW\PhantomJs\Parser\JsonParser;
@@ -135,6 +136,28 @@ class ProcedureLoaderTest extends \PHPUnit_Framework_TestCase
         $this->assertSame($body, $procedureLoader->load('test')->getTemplate());
     }
 
+    /**
+     * Test procedure template can be loaded.
+     *
+     * @access public
+     * @return void
+     */
+    public function testProcedureTemplateCanBeLoaded()
+    {
+        $body = 'TEST_PROCEDURE';
+        $file = $this->writeProcedure($body);
+
+        $procedureFactory = $this->getProcedureFactory();
+        $fileLocator      = $this->getFileLocator();
+
+        $fileLocator->method('locate')
+            ->will($this->returnValue($file));
+
+        $procedureLoader = $this->getProcedureLoader($procedureFactory, $fileLocator);
+
+        $this->assertNotNull($procedureLoader->loadTemplate('test'));
+    }
+
 /** +++++++++++++++++++++++++++++++++++ **/
 /** ++++++++++ TEST ENTITIES ++++++++++ **/
 /** +++++++++++++++++++++++++++++++++++ **/
@@ -165,15 +188,29 @@ class ProcedureLoaderTest extends \PHPUnit_Framework_TestCase
      */
     protected function getProcedureFactory()
     {
+        $engine   = $this->getEngine();
         $parser   = $this->getParser();
         $cache    = $this->getCache();
         $renderer = $this->getRenderer();
 
-        $procedureFactory = new ProcedureFactory($parser, $cache, $renderer);
+        $procedureFactory = new ProcedureFactory($engine, $parser, $cache, $renderer);
 
         return $procedureFactory;
     }
 
+    /**
+     * Get engine.
+     *
+     * @access protected
+     * @return \JonnyW\PhantomJs\Engine
+     */
+    protected function getEngine()
+    {
+        $engine = new Engine();
+
+        return $engine;
+    }
+
     /**
      * Get parser.
      *

+ 20 - 17
src/JonnyW/PhantomJs/Tests/Unit/Procedure/ProcedureTest.php

@@ -10,6 +10,7 @@ namespace JonnyW\PhantomJs\Tests\Unit\Procedure;
 
 use Twig_Environment;
 use Twig_Loader_String;
+use JonnyW\PhantomJs\Engine;
 use JonnyW\PhantomJs\Cache\FileCache;
 use JonnyW\PhantomJs\Cache\CacheInterface;
 use JonnyW\PhantomJs\Parser\JsonParser;
@@ -43,11 +44,12 @@ class ProcedureTest extends \PHPUnit_Framework_TestCase
     {
         $template = 'PROCEDURE_TEMPLATE';
 
+        $engne     = $this->getEngine();
         $parser    = $this->getParser();
         $cache     = $this->getCache();
         $renderer  = $this->getRenderer();
 
-        $procedure = $this->getProcedure($parser, $cache, $renderer);
+        $procedure = $this->getProcedure($engne, $parser, $cache, $renderer);
         $procedure->setTemplate($template);
 
         $this->assertSame($procedure->getTemplate(), $template);
@@ -63,6 +65,7 @@ class ProcedureTest extends \PHPUnit_Framework_TestCase
     {
         $template = 'TEST_{{ input.get("uncompiled") }}_PROCEDURE';
 
+        $engne     = $this->getEngine();
         $parser    = $this->getParser();
         $cache     = $this->getCache();
         $renderer  = $this->getRenderer();
@@ -70,7 +73,7 @@ class ProcedureTest extends \PHPUnit_Framework_TestCase
         $input  = $this->getInput();
         $input->set('uncompiled', 'COMPILED');
 
-        $procedure = $this->getProcedure($parser, $cache, $renderer);
+        $procedure = $this->getProcedure($engne, $parser, $cache, $renderer);
         $procedure->setTemplate($template);
 
         $this->assertSame('TEST_COMPILED_PROCEDURE', $procedure->compile($input));
@@ -87,17 +90,17 @@ class ProcedureTest extends \PHPUnit_Framework_TestCase
     {
         $this->setExpectedException('\JonnyW\PhantomJs\Exception\NotWritableException');
 
+        $engne    = $this->getEngine();
         $parser   = $this->getParser();
         $renderer = $this->getRenderer();
 
         $cache = $this->getCache('/an/invalid/dir');
 
-        $client = $this->getClient();
         $input  = $this->getInput();
         $output = $this->getOutput();
 
-        $procedure = $this->getProcedure($parser, $cache, $renderer);
-        $procedure->run($client, $input, $output);
+        $procedure = $this->getProcedure($engne, $parser, $cache, $renderer);
+        $procedure->run($input, $output);
     }
 
     /**
@@ -117,12 +120,12 @@ class ProcedureTest extends \PHPUnit_Framework_TestCase
         $input    = $this->getInput();
         $output   = $this->getOutput();
 
-        $client = $this->getClient();
-        $client->method('getCommand')
+        $engne = $this->getEngine();
+        $engne->method('getCommand')
             ->will($this->throwException(new \Exception()));
 
-        $procedure = $this->getProcedure($parser, $cache, $renderer);
-        $procedure->run($client, $input, $output);
+        $procedure = $this->getProcedure($engne, $parser, $cache, $renderer);
+        $procedure->run($input, $output);
     }
 
 /** +++++++++++++++++++++++++++++++++++ **/
@@ -133,14 +136,15 @@ class ProcedureTest extends \PHPUnit_Framework_TestCase
      * Get procedure instance.
      *
      * @access protected
+     * @param  \JonnyW\PhantomJs\Engine                             $engine
      * @param  \JonnyW\PhantomJs\Parser\ParserInterface             $parser
      * @param  \JonnyW\PhantomJs\Cache\CacheInterface               $cacheHandler
      * @param  \JonnyW\PhantomJs\Template\TemplateRendererInterface $renderer
      * @return \JonnyW\PhantomJs\Procedure\Procedure
      */
-    protected function getProcedure(ParserInterface $parser, CacheInterface $cacheHandler, TemplateRendererInterface $renderer)
+    protected function getProcedure(Engine $engine, ParserInterface $parser, CacheInterface $cacheHandler, TemplateRendererInterface $renderer)
     {
-        $procedure = new Procedure($parser, $cacheHandler, $renderer);
+        $procedure = new Procedure($engine, $parser, $cacheHandler, $renderer);
 
         return $procedure;
     }
@@ -221,16 +225,15 @@ class ProcedureTest extends \PHPUnit_Framework_TestCase
 /** +++++++++++++++++++++++++++++++++++ **/
 
     /**
-     * Get client.
+     * Get engine
      *
      * @access protected
-     * @return \JonnyW\PhantomJs\ClientInterface
+     * @return \JonnyW\PhantomJs\Engine
      */
-    protected function getClient()
+    protected function getEngine()
     {
-        $client = $this->getMock('\JonnyW\PhantomJs\ClientInterface');
+        $engine = $this->getMock('\JonnyW\PhantomJs\Engine');
 
-        return $client;
+        return $engine;
     }
-
 }