commit 941a14909798784ec7f16914cb631b61a144650c Author: Maxime Renou Date: Wed Jul 7 10:24:05 2021 +0200 version 1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5408cab --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +vendor/ +.DS_Store diff --git a/StorageBundle/Adaptors/S3Storage.php b/StorageBundle/Adaptors/S3Storage.php new file mode 100644 index 0000000..8001821 --- /dev/null +++ b/StorageBundle/Adaptors/S3Storage.php @@ -0,0 +1,175 @@ +configIsNormed($config))) + throw new InvalidStorageConfiguration("Invalid $storage_name storage configuration"); + + $this->config = $config; + $this->bucket = $config['bucket']; + $this->bucket_url = $config['bucket_url']; + + $this->client = new S3Client([ + 'version' => isset($config['version']) ? $config['version'] : 'latest', + 'region' => $config['region'], + 'endpoint' => $config['endpoint'], + 'credentials' => [ + 'key' => $config['credentials']['key'], + 'secret' => $config['credentials']['secret'] + ] + ]); + } + + public function getClient() + { + return $this->client; + } + + public function mode($name) + { + if ($name == 'public') return self::MODE_PUBLIC; + return self::MODE_PRIVATE; + } + + private function configIsNormed($config) + { + return ( + isset($config['type']) && + isset($config['bucket']) && + isset($config['bucket_url']) && + isset($config['region']) && + isset($config['endpoint']) && + isset($config['credentials']) && + isset($config['credentials']['key']) && + isset($config['credentials']['secret']) + ); + } + + protected function getPrefix($prefix = null) + { + $ret = ''; + if (isset($this->config['path']) && !empty($this->config['path'])) + { + $ret = trim($this->config['path'], '/').'/'; + if (trim($ret) == '/') $ret = ''; + } + if (!is_null($prefix)) { + $ret = ltrim($prefix, '/'); + } + return $ret; + } + + public function index($prefix = null) + { + return ($this->client->listObjectsV2([ + 'Bucket' => $this->bucket, + 'Prefix' => $this->getPrefix($prefix) + ])); + } + + /** + * Permet d'obtenir l'URL vers une ressource S3 + * Cette ressource n'est accessible que si le fichier a été stocké en mode public + * @param $target_path + * @return string + */ + public function url ($target_path) + { + return rtrim($this->bucket_url, '/').'/'.$this->getPrefix().ltrim($target_path, '/'); + } + + /** + * Permet de stocker un fichier dans S3 + * @param $source_path + * @param $target_path + * @param string $permissions + * @return \Aws\Result + */ + public function store ($source_path, $target_path, $permissions = self::MODE_PRIVATE) + { + return $this->client->putObject([ + 'Bucket' => $this->bucket, + 'Path' => $this->getPrefix().$target_path, + 'Key' => $this->getPrefix().$target_path, + 'SourceFile' => $source_path, // Fix memory allocation + //'Body' => file_get_contents($source_path), + 'ACL' => $permissions + ]); + } + + /** + * Permet de récupérer un fichier dans S3 pour le stocker en local + * @param $distant_path + * @param $local_path + */ + public function retrieve ($distant_path, $local_path) + { + $file_stream = fopen($local_path, 'w'); + + $aws_stream = $this->client->getObject([ + 'Bucket' => $this->bucket, + 'Key' => $this->getPrefix().$distant_path, + 'Path' => $this->getPrefix().$distant_path, + ])->get('Body')->detach(); + + stream_copy_to_stream($aws_stream, $file_stream); + fclose($file_stream); + } + + /** + * Ouvre le stream d'un fichier stocké dans S3 + * @param $distant_path + * @param $target_stream + */ + public function getStream ($distant_path) + { + return $this->client->getObject([ + 'Bucket' => $this->bucket, + 'Key' => $this->getPrefix().$distant_path, + 'Path' => $this->getPrefix().$distant_path, + ])->get('Body')->detach(); + } + + /** + * Permet de stream un fichier stocké dans S3 + * @param $distant_path + * @param $target_stream + */ + public function stream ($distant_path, $target_stream) + { + stream_copy_to_stream($this->getStream($distant_path), $target_stream); + } + + /** + * Permet de supprimer un fichier stocké dans S3 + * @param $distant_path + */ + public function delete ($distant_path) + { + $this->client->deleteObject([ + 'Bucket' => $this->bucket, + 'Path' => $this->getPrefix().$distant_path, + 'Key' => $this->getPrefix().$distant_path, + ]); + } +} diff --git a/StorageBundle/Adaptors/StorageAdaptor.php b/StorageBundle/Adaptors/StorageAdaptor.php new file mode 100644 index 0000000..37fcb35 --- /dev/null +++ b/StorageBundle/Adaptors/StorageAdaptor.php @@ -0,0 +1,13 @@ +root('storage'); + $root->useAttributeAsKey('storage_name') + ->prototype('array') + ->children() + ->scalarNode('type')->isRequired()->cannotBeEmpty()->end() + ->scalarNode('bucket')->end() + ->scalarNode('bucket_url')->end() + ->scalarNode('region')->end() + ->scalarNode('endpoint')->end() + ->arrayNode('credentials') + ->children() + ->scalarNode('key')->end() + ->scalarNode('secret')->end() + ->end() + ->end() + ->scalarNode('version')->end() + ->scalarNode('path')->end() + ->end() + ->end() + ->end(); + return ($treeBuilder); + } +} diff --git a/StorageBundle/DependencyInjection/StorageExtension.php b/StorageBundle/DependencyInjection/StorageExtension.php new file mode 100644 index 0000000..0b2f60f --- /dev/null +++ b/StorageBundle/DependencyInjection/StorageExtension.php @@ -0,0 +1,42 @@ +load('services.yaml'); + + $configuration = $this->getConfiguration($configs, $container); + + $config = $this->processConfiguration($configuration, $configs); + + $definition = $container->getDefinition('bluesquare.storage'); + $definition->setArgument(0, $config); + + return $config; + } + + + public function getAlias() + { +// return ('bluesquare\\storage'); + return parent::getAlias(); // TODO: Change the autogenerated stub + } +} diff --git a/StorageBundle/Exceptions/InvalidFileException.php b/StorageBundle/Exceptions/InvalidFileException.php new file mode 100644 index 0000000..391f291 --- /dev/null +++ b/StorageBundle/Exceptions/InvalidFileException.php @@ -0,0 +1,8 @@ +config_storage = $user_config; + } + + public function get($storage_name) + { + if (array_key_exists($storage_name, $this->config_storage)) + { + $config = $this->config_storage[$storage_name]; + + switch ($config['type']) + { + case 's3': return (new S3Storage($storage_name, $config)); + } + } + + return (null); + } + + protected function getStorageAnnotation($entity, $attribute) + { + $reflection = new \ReflectionProperty($entity, $attribute); + + $reader = new AnnotationReader(); + $annotations = $reader->getPropertyAnnotations($reflection); + + $storage_annotation = null; + + foreach ($annotations as $annotation) + { + if ($annotation instanceof StorageAnnotation) { + $storage_annotation = $annotation; + } + } + + if (is_null($storage_annotation)) + { + throw new MissingStorageAnnotation("Missing Storage annotation for $attribute in ".get_class($entity)); + } + + return $storage_annotation; + } + + public function getStorageFor($entity, $attribute) + { + $annotation = $this->getStorageAnnotation($entity, $attribute); + return $annotation->name; + } + + public function url($entity, $attribute, $class = null) + { + $annotation = $this->getStorageAnnotation(!is_null($class) ? $class : $entity, $attribute); + $storage = $this->get($annotation->name); + $prefix = is_null($annotation->prefix) || empty($annotation->prefix) ? '' : trim($annotation->prefix, '/').'/'; + $camel = ucfirst(Container::camelize($attribute)); + return $storage->url("$prefix{$entity->{"get".$camel}()}"); + } + + public function delete($entity, $attribute) + { + $annotation = $this->getStorageAnnotation($entity, $attribute); + $storage = $this->get($annotation->name); + $prefix = is_null($annotation->prefix) || empty($annotation->prefix) ? '' : trim($annotation->prefix, '/').'/'; + $camel = ucfirst(Container::camelize($attribute)); + return $storage->delete("$prefix{$entity->{"get".$camel}()}"); + } + + public function retrieve($entity, $attribute, $local_path) + { + $annotation = $this->getStorageAnnotation($entity, $attribute); + $storage = $this->get($annotation->name); + $prefix = is_null($annotation->prefix) || empty($annotation->prefix) ? '' : trim($annotation->prefix, '/').'/'; + $camel = ucfirst(Container::camelize($attribute)); + return $storage->retrieve("$prefix{$entity->{"get".$camel}()}", $local_path); + } + + public function stream($entity, $attribute, $target_stream = null) + { + $annotation = $this->getStorageAnnotation($entity, $attribute); + $storage = $this->get($annotation->name); + $prefix = is_null($annotation->prefix) || empty($annotation->prefix) ? '' : trim($annotation->prefix, '/').'/'; + $camel = ucfirst(Container::camelize($attribute)); + + if (is_null($target_stream)) { + return $storage->getStream("$prefix{$entity->{"get".$camel}()}"); + } + + return $storage->stream("$prefix{$entity->{"get".$camel}()}", $target_stream); + } + + public function store($entity, $attribute, $file) + { + $storage_annotation = $this->getStorageAnnotation($entity, $attribute); + $file_hash = hash('sha256', time().$attribute.uniqid()); + $storage = $this->get($storage_annotation->name); + + if (is_null($storage)) + { + throw new UnknownStorage("Unknown storage {$storage_annotation->name} for $attribute in ".get_class($entity)); + } + + $prefix = is_null($storage_annotation->prefix) || empty($storage_annotation->prefix) ? '' : trim($storage_annotation->prefix, '/').'/'; + $mode = $storage->mode($storage_annotation->mode); + $camel = ucfirst(Container::camelize($attribute)); + + if ($file instanceof UploadedFile) { + $file_hash .= strlen($file->getClientOriginalExtension()) > 0 ? '.'.$file->getClientOriginalExtension() : ''; + + if (!is_null($storage_annotation->mime)) + { + $valid = true; + if (count(explode('/', $storage_annotation->mime)) > 1) { + $valid = strtolower($storage_annotation->mime) == $file->getMimeType(); + } + else { + $valid = strtolower($storage_annotation->mime) == explode('/', $file->getMimeType())[0]; + } + + if (!$valid) { + throw new MimeTypeException("Invalid mime type"); + } + } + + $storage->store($file->getRealPath(), "$prefix$file_hash", $mode); + $previous_file_hash = $entity->{"get$camel"}(); + if (!is_null($previous_file_hash) && !empty($previous_file_hash)) { + $storage->delete("$prefix$previous_file_hash"); + } + } + elseif (is_string($file) && file_exists($file)) { + $storage->store($file, "$prefix$file_hash", $mode); + } + else { + throw new InvalidFileException("Invalid file argument"); + } + + $entity->{"set$camel"}($file_hash); + + return $file_hash; + } +} diff --git a/StorageBundle/StorageBundle.php b/StorageBundle/StorageBundle.php new file mode 100644 index 0000000..033f356 --- /dev/null +++ b/StorageBundle/StorageBundle.php @@ -0,0 +1,22 @@ +extension) + $this->extension = new StorageExtension(); + return $this->extension; + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..3d4ddac --- /dev/null +++ b/composer.json @@ -0,0 +1,23 @@ +{ + "name": "bluesquare-packages/symfony-storage", + "description": "Cryptor created by Bluesquare Computing", + "keywords": ["template", "composer", "package"], + "license": "proprietary", + "authors": [ + { + "name": "RENOU Maxime", + "email": "maxime@bluesquare.io" + } + ], + "type": "symfony-bundle", + "require": { + "php": ">=7.1", + "aws/aws-sdk-php": "^3.81", + "doctrine/event-manager": "*" + }, + "autoload": { + "psr-4": { + "Bluesquare\\StorageBundle\\": "StorageBundle/" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..8d8311e --- /dev/null +++ b/composer.lock @@ -0,0 +1,504 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "e0fd4d7a1648530618008debe3b39eb2", + "packages": [ + { + "name": "aws/aws-sdk-php", + "version": "3.87.15", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "21a4dd314e2a3c44b5538dd56d3770733a3d03e6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/21a4dd314e2a3c44b5538dd56d3770733a3d03e6", + "reference": "21a4dd314e2a3c44b5538dd56d3770733a3d03e6", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^5.3.3|^6.2.1", + "guzzlehttp/promises": "~1.0", + "guzzlehttp/psr7": "^1.4.1", + "mtdowling/jmespath.php": "~2.2", + "php": ">=5.5" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "nette/neon": "^2.3", + "phpunit/phpunit": "^4.8.35|^5.4.3", + "psr/cache": "^1.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Aws\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "time": "2019-02-20T19:11:08+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/a520bc093a0170feeb6b14e9d83f3a14452e64b3", + "reference": "a520bc093a0170feeb6b14e9d83f3a14452e64b3", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "conflict": { + "doctrine/common": "<2.9@dev" + }, + "require-dev": { + "doctrine/coding-standard": "^4.0", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "Doctrine Event Manager component", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "eventdispatcher", + "eventmanager" + ], + "time": "2018-06-11T11:59:03+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.3-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2018-04-22T15:46:56+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20T10:07:11+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.5.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "9f83dded91781a01c63574e387eaa769be769115" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", + "reference": "9f83dded91781a01c63574e387eaa769be769115", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2018-12-04T20:46:45+00:00" + }, + { + "name": "mtdowling/jmespath.php", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/adcc9531682cf87dfda21e1fd5d0e7a41d292fac", + "reference": "adcc9531682cf87dfda21e1fd5d0e7a41d292fac", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "JmesPath\\": "src/" + }, + "files": [ + "src/JmesPath.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "time": "2016-12-03T22:08:25+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~3.7.0", + "satooshi/php-coveralls": ">=1.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2016-02-11T07:05:27+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.1" + }, + "platform-dev": [] +}