├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── AbstractPackage.php ├── InstalledPackagesFile.php ├── JsonObject.php ├── LockFile.php ├── Package.php ├── PackageCollection.php ├── RootPackage.php ├── functions.php ├── functions_include.php └── functions_internal.php /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.idea/ 3 | /nbproject/ 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## Unreleased 6 | ### Added 7 | - `VENDOR_DIR` and `BASE_DIR` constants 8 | - `packages()`, `package()`, `package_configs()`, `package_config()` and `project_config()` functions 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Joshua Di Fabio 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Composed 2 | 3 | [![Code Quality](https://img.shields.io/scrutinizer/g/joshdifabio/composed.svg?style=flat-square)](https://scrutinizer-ci.com/g/joshdifabio/composed/) 4 | 5 | This library provides a set of utility functions designed to help you parse your project's Composer configuration, and those of its dependencies, at runtime. 6 | 7 | ## Usage 8 | 9 | The API combines functional and object-oriented approaches. 10 | 11 | ### Locate the vendor directory 12 | 13 | (Chicken and egg...) 14 | 15 | ```php 16 | $absoluteVendorPath = Composed\VENDOR_DIR; 17 | ``` 18 | 19 | ### Locate the project's base directory 20 | 21 | ```php 22 | $absoluteProjectPath = Composed\BASE_DIR; 23 | ``` 24 | 25 | ### Get the authors of a specific package 26 | 27 | You can fetch data from the `composer.json` file of a specific package. 28 | 29 | ```php 30 | $authors = Composed\package_config('phpunit/phpunit', 'authors'); 31 | 32 | assert($authors === [ 33 | [ 34 | 'name' => "Sebastian Bergmann", 35 | 'email' => "sebastian@phpunit.de", 36 | 'role' => "lead", 37 | ], 38 | ]); 39 | ``` 40 | 41 | ### Get licenses of all installed packages 42 | 43 | You can fetch data from all `composer.json` files in your project in one go. 44 | 45 | ```php 46 | $licenses = Composed\package_configs('license'); 47 | 48 | assert($licenses === [ 49 | 'joshdifabio/composed' => "MIT", 50 | 'doctrine/instantiator' => "MIT", 51 | 'phpunit/php-code-coverage' => "BSD-3-Clause", 52 | ]); 53 | ``` 54 | 55 | ### Get the absolute path to a file in a package 56 | 57 | ```php 58 | $path = Composed\package('phpunit/phpunit')->getPath('composer.json'); 59 | ``` 60 | 61 | ### Get all packages installed on your project 62 | 63 | ```php 64 | foreach (Composed\packages() as $packageName => $package) { 65 | $pathToPackageConfig = $package->getPath('composer.json'); 66 | // ... 67 | } 68 | ``` 69 | 70 | ### Get data from your project's Composer config 71 | 72 | You can also fetch data from the `composer.json` file located in your project root. 73 | 74 | ```php 75 | $projectAuthors = Composed\project_config('authors'); 76 | 77 | assert($projectAuthors === [ 78 | [ 79 | 'name' => 'Josh Di Fabio', 80 | 'email' => 'joshdifabio@somewhere.com', 81 | ], 82 | ]); 83 | ``` 84 | 85 | ## Installation 86 | 87 | Install Composed using [composer](https://getcomposer.org/). 88 | 89 | ``` 90 | composer require joshdifabio/composed 91 | ``` 92 | 93 | ## Credits 94 | 95 | Credit goes to @igorw whose [get-in](https://github.com/igorw/get-in) library is partially copied into this library. Unfortunately, `igorw/get-in` requires PHP 5.4 while Composed aims for PHP 5.3 compatibility. 96 | 97 | ## License 98 | 99 | Composed is released under the [MIT](https://github.com/joshdifabio/composed/blob/master/LICENSE) license. 100 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joshdifabio/composed", 3 | "description": "Easily parse your project's Composer configuration, and those of its dependencies, at runtime.", 4 | "keywords": [ "composer", "dependency", "package" ], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Josh Di Fabio", 9 | "email": "joshdifabio@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=7" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Composed\\": "src" 18 | }, 19 | "files": [ 20 | "src/functions_include.php" 21 | ] 22 | }, 23 | "extra": { 24 | "branch-alias": { 25 | "dev-master": "2.0-dev" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/AbstractPackage.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | abstract class AbstractPackage 8 | { 9 | private $dirPath; 10 | private $config; 11 | private $root; 12 | private $lockFile; 13 | private $directDependencies; 14 | private $dependencies; 15 | 16 | protected function __construct(RootPackage $root, string $dirPath, JsonObject $config) 17 | { 18 | $this->root = $root; 19 | $this->dirPath = $dirPath; 20 | $this->config = $config; 21 | } 22 | 23 | /** 24 | * @return null|string 25 | */ 26 | public function getName(bool $includeVendorName = true) 27 | { 28 | $name = $this->config->get(['name']); 29 | 30 | if ($includeVendorName) { 31 | return $name; 32 | } 33 | 34 | return ltrim(strstr($name, '/'), '/'); 35 | } 36 | 37 | /** 38 | * @return null|string 39 | */ 40 | public function getVendorName() 41 | { 42 | if (null === $name = $this->getName()) { 43 | return null; 44 | } 45 | 46 | return strstr($name, '/', true); 47 | } 48 | 49 | public function isRoot() : bool 50 | { 51 | return $this->root === $this; 52 | } 53 | 54 | /** 55 | * @return mixed 56 | */ 57 | public function getConfig($keys = [], $default = null) 58 | { 59 | return $this->config->get($keys, $default); 60 | } 61 | 62 | public function getPath(string $relativePath = '') : string 63 | { 64 | return $this->dirPath . (strlen($relativePath) ? DIRECTORY_SEPARATOR . $relativePath : ''); 65 | } 66 | 67 | /** 68 | * @return null|LockFile 69 | */ 70 | public function getLockFile() 71 | { 72 | if (null === $this->lockFile) { 73 | $filePath = $this->getPath('composer.lock'); 74 | if (file_exists($filePath)) { 75 | $this->lockFile = LockFile::fromFilePath($this->root, $filePath); 76 | } 77 | } 78 | 79 | return $this->lockFile; 80 | } 81 | 82 | public function directlyRequires(string $packageName) : bool 83 | { 84 | return null !== $this->getDirectDependencies()->getPackage($packageName); 85 | } 86 | 87 | public function requires(string $packageName) : bool 88 | { 89 | return null !== $this->getDependencies()->getPackage($packageName); 90 | } 91 | 92 | public function getDirectDependencies() : PackageCollection 93 | { 94 | if (null === $this->directDependencies) { 95 | $packageNames = array_keys($this->getConfig('require', $default = [])); 96 | $packages = @array_map( 97 | function ($packageName) { 98 | return $this->root->getPackages()->getPackage($packageName); 99 | }, 100 | $packageNames 101 | ); 102 | $packages = array_combine($packageNames, $packages); 103 | $packages = array_filter($packages); 104 | $this->directDependencies = new PackageCollection($packages); 105 | } 106 | 107 | return $this->directDependencies; 108 | } 109 | 110 | public function getDependencies() : PackageCollection 111 | { 112 | if (null === $this->dependencies) { 113 | $packages = []; 114 | /** @var Package $package */ 115 | foreach ($this->getDirectDependencies() as $name => $package) { 116 | $packages[$name] = $package; 117 | $packages = array_merge($packages, $package->getDependencies()->toArray()); 118 | } 119 | $this->dependencies = new PackageCollection($packages); 120 | } 121 | 122 | return $this->dependencies; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/InstalledPackagesFile.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class InstalledPackagesFile 8 | { 9 | private $root; 10 | private $json; 11 | private $packages; 12 | 13 | public function __construct(RootPackage $root, JsonObject $json) 14 | { 15 | $this->root = $root; 16 | $this->json = $json; 17 | } 18 | 19 | public static function fromFilePath(RootPackage $root, string $filePath) : self 20 | { 21 | return new self($root, JsonObject::fromFilePath($filePath)); 22 | } 23 | 24 | public function getPackages() : PackageCollection 25 | { 26 | if (null === $this->packages) { 27 | $packages = []; 28 | $packagesData = $this->json->get(); 29 | foreach ($packagesData as $packageData) { 30 | $package = Package::fromArray($this->root, $packageData); 31 | $packages[$package->getName()] = $package; 32 | } 33 | $this->packages = new PackageCollection($packages); 34 | } 35 | 36 | return $this->packages; 37 | } 38 | 39 | public function get($keys = [], $default = null) 40 | { 41 | return $this->json->get($keys, $default); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/JsonObject.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class JsonObject 8 | { 9 | private $data; 10 | 11 | public function __construct(array $data) 12 | { 13 | $this->data = $data; 14 | } 15 | 16 | public static function fromFilePath(string $filePath) : self 17 | { 18 | if (false === $fileContent = @file_get_contents($filePath)) { 19 | if (!file_exists($filePath)) { 20 | throw new \RuntimeException("File not found: $filePath"); 21 | } 22 | 23 | throw new \RuntimeException("Failed to open file: $filePath"); 24 | } 25 | 26 | if (null === $data = json_decode($fileContent, true)) { 27 | throw new \RuntimeException("File does not contain valid JSON: $filePath"); 28 | } 29 | 30 | return self::fromArray($data); 31 | } 32 | 33 | public static function fromArray(array $data) : self 34 | { 35 | return new self($data); 36 | } 37 | 38 | public function get($keys = [], $default = null) 39 | { 40 | if (!is_array($keys)) { 41 | $keys = [$keys]; 42 | } 43 | 44 | if (!$keys) { 45 | return $this->data; 46 | } 47 | 48 | return $this->deepGet($this->data, $keys, $default); 49 | } 50 | 51 | private function deepGet(array $current, array $keys = [], $default = null) 52 | { 53 | foreach ($keys as $key) { 54 | if (!is_array($current) || !array_key_exists($key, $current)) { 55 | return $default; 56 | } 57 | $current = $current[$key]; 58 | } 59 | 60 | return $current; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/LockFile.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class LockFile 8 | { 9 | private $root; 10 | private $json; 11 | private $packages; 12 | 13 | public function __construct(RootPackage $root, JsonObject $json) 14 | { 15 | $this->root = $root; 16 | $this->json = $json; 17 | } 18 | 19 | public static function fromFilePath(RootPackage $root, string $filePath) : self 20 | { 21 | return new self($root, JsonObject::fromFilePath($filePath)); 22 | } 23 | 24 | public function getPackages() : PackageCollection 25 | { 26 | if (null === $this->packages) { 27 | $packages = []; 28 | $packagesData = $this->json->get(['packages'], []); 29 | foreach ($packagesData as $packageData) { 30 | $package = Package::fromArray($this->root, $packageData); 31 | $packages[$package->getName()] = $package; 32 | } 33 | $this->packages = new PackageCollection($packages); 34 | } 35 | 36 | return $this->packages; 37 | } 38 | 39 | public function getDevPackages() : PackageCollection 40 | { 41 | if (null === $this->packages) { 42 | $packages = []; 43 | $packagesData = $this->json->get(['packages-dev'], []); 44 | foreach ($packagesData as $packageData) { 45 | $package = Package::fromArray($this->root, $packageData); 46 | $packages[$package->getName()] = $package; 47 | } 48 | $this->packages = new PackageCollection($packages); 49 | } 50 | 51 | return $this->packages; 52 | } 53 | 54 | public function get($keys = [], $default = null) 55 | { 56 | return $this->json->get($keys, $default); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Package.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class Package extends AbstractPackage 8 | { 9 | public static function create(RootPackage $root, JsonObject $packageConfig) : self 10 | { 11 | if (null === $packageName = $packageConfig->get('name')) { 12 | throw new \InvalidArgumentException('Package data must include package name'); 13 | } 14 | $dirPath = $root->getVendorPath(str_replace('/', DIRECTORY_SEPARATOR, $packageConfig->get('name'))); 15 | 16 | return new static($root, $dirPath, $packageConfig); 17 | } 18 | 19 | public static function fromArray(RootPackage $root, array $packageConfig) : self 20 | { 21 | return self::create($root, JsonObject::fromArray($packageConfig)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/PackageCollection.php: -------------------------------------------------------------------------------- 1 | packages = $packages; 14 | } 15 | 16 | /** 17 | * @return \Iterator 18 | */ 19 | public function getIterator() 20 | { 21 | return new \ArrayIterator($this->packages); 22 | } 23 | 24 | /** 25 | * @return null|Package 26 | */ 27 | public function getPackage(string $name) 28 | { 29 | return isset($this->packages[$name]) ? $this->packages[$name] : null; 30 | } 31 | 32 | /** 33 | * Get a config value from all packages 34 | * 35 | * @param string|array $keys Either a string, e.g. 'required' to get the dependencies by a package, or an array, 36 | * e.g. ['required', 'php'] to get the version of PHP required by a package 37 | * 38 | * @param mixed $default If this is not explicitly specified, packages which do not provide any config value will be 39 | * omitted from the result. Explicitly setting this to NULL is not the same as omitting it and will result in 40 | * packages which do not provide config being returned with a value of NULL 41 | * 42 | * @return array Keys are package names, values are the retrieved config as an array or scalar 43 | */ 44 | public function getConfig($keys = [], $default = null) : array 45 | { 46 | if (2 > func_num_args()) { 47 | $default = new \stdClass; 48 | } 49 | 50 | $config = @array_map( 51 | function (AbstractPackage $package) use ($keys, $default) { 52 | return $package->getConfig($keys, $default); 53 | }, 54 | $this->packages 55 | ); 56 | 57 | if (2 > func_num_args()) { 58 | $config = array_filter($config, function ($value) use ($default) { 59 | return $value !== $default; 60 | }); 61 | } 62 | 63 | return $config; 64 | } 65 | 66 | public function toArray() : array 67 | { 68 | return $this->packages; 69 | } 70 | 71 | public function getPackageNames() : array 72 | { 73 | return array_keys($this->packages); 74 | } 75 | 76 | /** 77 | * Returns all packages sorted based on their dependencies. Each package is guaranteed to appear after all of its 78 | * dependencies in the collection 79 | */ 80 | public function sortByDependencies() : self 81 | { 82 | /** @var $packages Package[] */ 83 | $packages = $this->packages; 84 | $sortedPackages = []; 85 | 86 | while (!empty($packages)) { 87 | foreach ($packages as $packageName => $package) { 88 | $dependentPackages = $package->getDependencies()->toArray(); 89 | if (empty(array_diff_key($dependentPackages, $sortedPackages))) { 90 | // all of this packages dependencies are already in the sorted array, so it can be appended 91 | $sortedPackages[$packageName] = $package; 92 | unset($packages[$packageName]); 93 | continue(2); 94 | } 95 | } 96 | throw new \LogicException('Packages have circular dependencies'); 97 | } 98 | 99 | return new static($sortedPackages); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/RootPackage.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | class RootPackage extends AbstractPackage 8 | { 9 | private $packages; 10 | private $installedPackagesFile; 11 | 12 | public function __construct(string $dirPath, JsonObject $config) 13 | { 14 | parent::__construct($this, $dirPath, $config); 15 | } 16 | 17 | public static function createFromPath(string $dirPath) : self 18 | { 19 | return new static($dirPath, JsonObject::fromFilePath($dirPath . DIRECTORY_SEPARATOR . 'composer.json')); 20 | } 21 | 22 | /** 23 | * Returns all packages in the project, including the root one 24 | */ 25 | public function getPackages() : PackageCollection 26 | { 27 | if (null === $this->packages) { 28 | $packages = array_merge( 29 | array( 30 | $this->getName() => $this, 31 | ), 32 | $this->getInstalledPackagesFile()->getPackages()->toArray() 33 | ); 34 | 35 | $this->packages = new PackageCollection($packages); 36 | } 37 | 38 | return $this->packages; 39 | } 40 | 41 | public function getLockFile() : LockFile 42 | { 43 | if (null === $lockFile = parent::getLockFile()) { 44 | throw new \RuntimeException('Lock file not found.'); 45 | } 46 | 47 | return $lockFile; 48 | } 49 | 50 | public function getInstalledPackagesFile() : InstalledPackagesFile 51 | { 52 | if (null === $this->installedPackagesFile) { 53 | $filePath = $this->getPath('vendor/composer/installed.json'); 54 | if (file_exists($filePath)) { 55 | $this->installedPackagesFile = InstalledPackagesFile::fromFilePath($this, $filePath); 56 | } 57 | } 58 | 59 | return $this->installedPackagesFile; 60 | } 61 | 62 | public function getVendorDir() : string 63 | { 64 | return 'vendor'; 65 | } 66 | 67 | public function getVendorPath(string $relativePath = '') : string 68 | { 69 | return $this->getPath($this->getVendorDir() . (strlen($relativePath) ? DIRECTORY_SEPARATOR . $relativePath : '')); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | getPackages() : project()->getLockFile()->getPackages(); 10 | } 11 | 12 | /** 13 | * @return null|AbstractPackage 14 | */ 15 | function package($name, $graceful = false) 16 | { 17 | $package = packages()->getPackage($name); 18 | 19 | if (!$graceful && !$package) { 20 | throw new \OutOfBoundsException('The specified package does not appear to be installed.'); 21 | } 22 | 23 | return $package; 24 | } 25 | 26 | function package_configs($keys = [], $default = null) : array 27 | { 28 | /** 29 | * If $default is not explicitly provided, it should not be passed to Package::getConfig(), 30 | * hence not simply calling getConfig($keys, $default), as explicitly passing $default = null 31 | * to getConfig() is different to omitting it 32 | */ 33 | return packages()->getConfig(...func_get_args()); 34 | } 35 | 36 | /** 37 | * @return mixed 38 | */ 39 | function package_config(string $packageName, $keys = [], $default = null) 40 | { 41 | return package($packageName)->getConfig($keys, $default); 42 | } 43 | 44 | /** 45 | * @return mixed 46 | */ 47 | function project_config($keys = [], $default = null) 48 | { 49 | return project()->getConfig($keys, $default); 50 | } 51 | 52 | function project(RootPackage $assign = null) : RootPackage 53 | { 54 | static $project; 55 | 56 | if ($assign) { 57 | $project = $assign; 58 | } elseif (!$project) { 59 | $project = RootPackage::createFromPath(BASE_DIR); 60 | } 61 | 62 | return $project; 63 | } 64 | -------------------------------------------------------------------------------- /src/functions_include.php: -------------------------------------------------------------------------------- 1 |