Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Once upon a time ...

  • Loading branch information...
commit 510e6b3142f80abe078b2b7e729e4f16aada633a 0 parents
@GromNaN authored
1  .gitignore
@@ -0,0 +1 @@
+vendor
66 README.md
@@ -0,0 +1,66 @@
+Buzy is a an HTTP client for PHP built on top of Symfony2 components
+====================================================================
+
+
+Requirements
+------------
+
+* PHP 5.3 +
+* Symfony HttpFoundation
+* Symfony Event Dispatcher
+* Curl Extension
+
+Usage
+-----
+
+```php
+
+$browser = new Buzy\Browser();
+$response = $browser->get('http://www.google.com');
+
+echo $browser->getLastRequest()."\n";
+echo $response;
+```
+
+You can also use the low-level HTTP classes directly.
+
+```php
+
+$request = new Symfony\Component\HttpFoundation\Request::create('http://google.com', 'GET');
+$response = new Symfony\Component\HttpFoundation\Response();
+
+$client = new Buzz\Client\FileGetContents();
+$client->send($request, $response);
+
+echo $request;
+echo $response;
+```
+
+Simple reverse proxy
+--------------------
+
+With this 4 lines of code, you can re-send a request and transfert response.
+
+```php
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Suzy\Browser;
+
+$request = Request::createFromGlobals();
+$request->server->set('HTTP_HOST', 'internal-server');
+
+$browser = new Browser();
+
+$response = $browser->send($request);
+
+$response->send();
+
+// The response is sent back to the client
+```
+
+Potential usages
+----------------
+
+* Resolve external ESI into a Symfony application without any cache server like Varnish
+*
15 composer.json
@@ -0,0 +1,15 @@
+{
+ "name": "grom/suzy",
+ "description": "Web client on top of Symfony2 HTTP foundation",
+ "require": {
+ "symfony/http-foundation": "*",
+ "symfony/event-dispatcher": "*",
+ "doctrine/common": "*"
+ },
+ "authors": [
+ {
+ "name": "Jerome Tamarelle",
+ "email": "jerome@tamarelle.net"
+ }
+ ]
+}
26 phpunit.php.dist
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit backupGlobals="false"
+ backupStaticAttributes="false"
+ colors="true"
+ convertErrorsToExceptions="true"
+ convertNoticesToExceptions="true"
+ convertWarningsToExceptions="true"
+ processIsolation="false"
+ stopOnFailure="false"
+ syntaxCheck="false"
+ bootstrap="tests/bootstrap.php"
+ >
+
+ <testsuites>
+ <testsuite name="Buzy Test Suite">
+ <directory>./tests/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory>./src/Buzy/</directory>
+ </whitelist>
+ </filter>
+
+</phpunit>
137 src/Buzy/Browser.php
@@ -0,0 +1,137 @@
+<?php
+
+namespace Buzy;
+
+use Buzy\Client\ClientInterface;
+use Buzy\Client\FileGetContents;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+use Symfony\Component\EventDispatcher\Event;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+class Browser
+{
+ private $client;
+ private $dispatcher;
+
+ public function __construct(ClientInterface $client = null, EventDispatcherInterface $dispatcher = null)
+ {
+ $this->client = $client ?: new FileGetContents();
+ $this->dispatcher = $dispatcher;
+ }
+
+ public function get($url, $headers = array())
+ {
+ return $this->call($url, 'GET', $headers);
+ }
+
+ public function post($url, $headers = array(), $content = '')
+ {
+ return $this->call($url, 'POST', $headers, $content);
+ }
+
+ public function head($url, $headers = array())
+ {
+ return $this->call($url, 'HEAD', $headers);
+ }
+
+ public function put($url, $headers = array(), $content = '')
+ {
+ return $this->call($url, 'PUT', $headers, $content);
+ }
+
+ public function delete($url, $headers = array(), $content = '')
+ {
+ return $this->call($url, 'DELETE', $headers, $content);
+ }
+
+ /**
+ * Sends a request.
+ *
+ * @param string $uri The URL to call
+ * @param string $method The request method to use
+ * @param array $headers An array of request headers
+ * @param string $content The request content
+ *
+ * @return Response The response object
+ */
+ public function call($uri, $method, $headers = array(), $content = '')
+ {
+ $request = Request::create($uri, $method, array(), array(), array(), array(), $content);
+
+ foreach ($headers as $key => $value) {
+ if (is_numeric($key)) {
+ list($key, $value) = explode(':', $value, 2);
+ }
+ $request->headers->set($name, $value);
+ }
+
+ return $this->send($request);
+ }
+
+ /**
+ * Sends a form request.
+ *
+ * @param string $url The URL to submit to
+ * @param array $fields An array of fields
+ * @param string $method The request method to use
+ * @param array $headers An array of request headers
+ *
+ * @return Response The response object
+ */
+ public function submit($url, array $fields, $method = 'POST', $headers = array())
+ {
+ $request = Request::create($uri, $method, $fields, array(), array(), array(), null);
+
+ foreach ($headers as $key => $value) {
+ if (is_numeric($key)) {
+ list($key, $value) = explode(':', $value, 2);
+ }
+ $request->headers->set($name, $value);
+ }
+
+ return $this->send($request);
+ }
+
+ /**
+ * Sends a request.
+ *
+ * @param Request $request A request object
+ * @param Response $response A response object
+ *
+ * @return Response A response object
+ */
+ public function send(Request $request, Response $response = null)
+ {
+ if (null === $response) {
+ $response = new Response();
+ }
+
+ if (null !== $this->dispatcher) {
+ $event = new BrowserEvent($request, $response);
+ $this->dispatcher->dispatch(BrowserEvent::REQUEST, $event);
+
+ if ($event->isPropagationStopped()) {
+ return $response;
+ }
+ }
+
+ $this->client->send($request, $response);
+
+ if (null !== $this->dispatcher) {
+ $this->dispatcher->dispatch(BrowserEvent::RESPONSE, $event);
+ }
+
+ return $response;
+ }
+
+ public function setClient(ClientInterface $client)
+ {
+ $this->client = $client;
+ }
+
+ public function getClient()
+ {
+ return $this->client;
+ }
+}
32 src/Buzy/BrowserEvent.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Buzy;
+
+use Symfony\Component\EventDispatcher\Event;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+class BrowserEvent extends Event
+{
+ const REQUEST = 'buzy.request';
+ const RESPONSE = 'buzy.response';
+
+ private $request;
+ private $response;
+
+ public function __construct(Request $request, Response $response)
+ {
+ $this->request = $request;
+ $this->response = $response;
+ }
+
+ public function getRequest()
+ {
+ return $this->request;
+ }
+
+ public function getResponse()
+ {
+ return $this->response;
+ }
+}
93 src/Buzy/Cache/CacheListener.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Buzy\Cache;
+
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Doctrine\Common\Cache\Cache;
+use Buzy\BrowserEvent;
+
+/**
+ * HTTP Cache listener provide standard cache features.
+ *
+ * @author Jérôme Tamarelle <jerome@tamarelle.net>
+ */
+class CacheListener implements EventSubscriberInterface
+{
+ /**
+ * @var \Doctrine\Common\Cache\Cache
+ */
+ private $cache;
+
+ /**
+ * Constructor.
+ *
+ * @param Doctrine\Common\Cache\Cache $cache
+ */
+ public function __construct(Cache $cache)
+ {
+ $this->cache = $cache;
+ }
+
+ /**
+ * Try to find a cached response before the resquest is sent
+ *
+ * @todo Implement HTTP cache rules
+ *
+ * @param \Buzy\BrowserEvent $event
+ */
+ public function onRequest(BrowserEvent $event)
+ {
+ $id = $this->generateRequestIdentifier($event->getRequest());
+
+ if ($this->cache->contains($id)) {
+ $cachedResponse = unserialize($this->cache->fetch($id));
+
+ $event->getResponse()->headers = clone $cachedResponse->headers;
+
+ $event->getResponse()
+ ->setContent($cachedResponse->getContent())
+ ->setContentType($cachedResponse->getContentType())
+ ->setProtocolVersion($cachedResponse->getProtocolVersion())
+ ->setStatusCode($cachedResponse->getStatusCode())
+ ->setEtag($cachedResponse->getEtag())
+ ->headers
+ ->set('Age', $cachedResponse->headers->get('Age') + time() - $cachedResponse->getDate()->format('U'))
+ ;
+
+ $event->stopPropagation();
+ }
+ }
+
+ /**
+ * Store the response if it is cachable.
+ *
+ * @param \Buzy\BrowserEvent $event
+ */
+ public function onResponse(BrowserEvent $event)
+ {
+ $response = $event->getResponse();
+ if ($response->isCacheable()) {
+ $id = $this->generateRequestIdentifier($event->getRequest());
+ $ttl = max($response->getTtl() - $response->getAge(), 0);
+ $this->cache->save($id, serialize($response));
+ }
+ }
+
+ protected function generateRequestIdentifier(Request $request)
+ {
+ return sha1($request->headers->all());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ static public function getSubscribedEvents()
+ {
+ return array(
+ BrowserEvent::REQUEST => 'onRequest',
+ BrowserEvent::RESPONSE => 'onResponse',
+ );
+ }
+}
17 src/Buzy/Client/ClientInterface.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Buzy\Client;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+interface ClientInterface
+{
+ /**
+ * Populates the supplied response with the response for the supplied request.
+ *
+ * @param Request $request A request object
+ * @param Response $response A response object
+ */
+ function send(Request $request, Response $response);
+}
84 src/Buzy/Client/FileGetContents.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Buzy\Client;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+class FileGetContents implements ClientInterface
+{
+ /**
+ * @see ClientInterface
+ *
+ * @throws RuntimeException If file_get_contents() fires an error
+ */
+ public function send(Request $request, Response $response)
+ {
+ $context = stream_context_create($x = $this->getStreamContextArray($request));
+
+ $uri = $request->getUri();
+ $level = error_reporting(0);
+ $content = file_get_contents($uri, 0, $context);
+ error_reporting($level);
+ if (false === $content) {
+ $error = error_get_last();
+ throw new \RuntimeException($error['message']);
+ }
+
+ $this->parseHeader((array) $http_response_header, $response);
+ $response->setContent($content);
+ }
+
+ /**
+ * Converts a request into an array for stream_context_create().
+ *
+ * @param Message\Request $request A request object
+ *
+ * @return array An array for stream_context_create()
+ */
+ protected function getStreamContextArray(Request $request)
+ {
+ return array(
+ 'http' => array(
+ // values from the request
+ 'method' => $request->getMethod(),
+ 'header' => strval($request->headers),
+ 'content' => $request->getContent(),
+ 'protocol_version' => $request->server->get('SERVER_PROTOCOL'),
+/*
+ // values from the current client
+ 'ignore_errors' => $this->getIgnoreErrors(),
+ 'max_redirects' => $this->getMaxRedirects(),
+ 'timeout' => $this->getTimeout(),
+*/
+ ),
+/*
+ 'ssl' => array(
+ 'verify_peer' => $this->getVerifyPeer(),
+ ),
+
+*/
+ );
+ }
+
+ protected function parseHeader(array $headers, Response $response)
+ {
+ // @todo cookies
+ foreach ($headers as $header) {
+ if (preg_match('#HTTP/([\.0-9]{3}) ([0-9]{3}) (.+)#', $header, $matches)) {
+ $response
+ ->setProtocolVersion($matches[1])
+ ->setStatusCode($matches[2], trim($matches[3]))
+ ;
+ } elseif (preg_match('#([\w-]+): (.+)#', $header, $matches)) {
+ $response->headers->set($matches[1], $matches[2]);
+ }
+ }
+
+ if ($response->headers->has('content-type')) {
+ if (preg_match('#.*;charset=(.*)#', $response->headers->get('content-type'), $matches)) {
+ $this->response->setCharset(trim($matches[1]));
+ }
+ }
+ }
+}
20 src/autoload.php
@@ -0,0 +1,20 @@
+<?php
+/*
+ * This file is part of the Buzy package.
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+/**
+ * Simple autoloader that follow partially the PHP Standards Recommendation #0 (PSR-0)
+ * @see https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md for more informations.
+ *
+ * @author Jérôme Tamarelle <jerome@tamarelle.net>
+ */
+
+spl_autoload_register(function($class) {
+ return 0 === strpos($class, 'Buzy')
+ && is_file($file = __DIR__ . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php')
+ && (bool) include $file;
+});
22 test.php
@@ -0,0 +1,22 @@
+<?php
+
+ini_set('display_errors', 'On');
+error_reporting(E_ALL);
+
+require __DIR__.'/vendor/.composer/autoload.php';
+require __DIR__.'/src/autoload.php';
+
+$cache = new Doctrine\Common\Cache\ArrayCache();
+$cacheListener = new Buzy\Cache\CacheListener($cache);
+
+$d = new Symfony\Component\EventDispatcher\EventDispatcher;
+$d->addSubscriber($cacheListener);
+$b = new Buzy\Browser(null, $d);
+
+//$response = $b->get('http://static.lexpress.fr/imgs/uploads/static/0e1/taneange_avatar_88x88.jpg');
+$response = $b->get('http://js.lexpress.fr/scripts/oas.js');
+var_dump($response->getAge());
+sleep(2);
+$response = $b->get('http://js.lexpress.fr/scripts/oas.js');
+var_dump($response->getAge());
+
Please sign in to comment.
Something went wrong with that request. Please try again.