PageRenderTime 106ms CodeModel.GetById 51ms app.highlight 16ms RepoModel.GetById 34ms app.codeStats 0ms

/sally/core/lib/sly/Service/Package.php

https://bitbucket.org/SallyCMS/trunk
PHP | 419 lines | 214 code | 69 blank | 136 comment | 31 complexity | f9df3df3a742c975413313ac949702c0 MD5 | raw file
  1<?php
  2/*
  3 * Copyright (c) 2012, webvariants GbR, http://www.webvariants.de
  4 *
  5 * This file is released under the terms of the MIT license. You can find the
  6 * complete text in the attached LICENSE file or online at:
  7 *
  8 * http://www.opensource.org/licenses/mit-license.php
  9 */
 10
 11/**
 12 * @author  christoph@webvariants.de
 13 * @ingroup service
 14 */
 15class sly_Service_Package {
 16	protected $sourceDir; ///< string
 17	protected $cache;     ///< BabelCache_Interface
 18	protected $composers; ///< array
 19	protected $refreshed; ///< boolean
 20	protected $namespace; ///< string
 21
 22	/**
 23	 * @param string               $sourceDir
 24	 * @param BabelCache_Interface $cache
 25	 */
 26	public function __construct($sourceDir, BabelCache_Interface $cache) {
 27		$this->sourceDir = sly_Util_Directory::normalize($sourceDir).DIRECTORY_SEPARATOR;
 28		$this->cache     = $cache;
 29		$this->composers = array();
 30		$this->refreshed = false;
 31		$this->namespace = substr(md5($this->sourceDir), 0, 10).'_';
 32	}
 33
 34	/**
 35	 * Clears the addOn metadata cache
 36	 */
 37	public function clearCache() {
 38		$this->cache->flush('sly.package', true);
 39		$this->composers = array();
 40		$this->refreshed = false;
 41	}
 42
 43	/**
 44	 * @param  string $package   package name
 45	 * @return string
 46	 */
 47	public function baseDirectory($package = null) {
 48		$dir = $this->sourceDir;
 49
 50		if (!empty($package)) {
 51			$dir .= str_replace('/', DIRECTORY_SEPARATOR, $package).DIRECTORY_SEPARATOR;
 52		}
 53
 54		return $dir;
 55	}
 56
 57	protected function getCacheKey($package, $key) {
 58		$package = str_replace('/', '%', $package);
 59		return ($package ? $package.'%' : '').$key;
 60	}
 61
 62	protected function getCache($package, $key, $default = null) {
 63		return $this->cache->get('sly.package.'.$this->namespace, $this->getCacheKey($package, $key), $default);
 64	}
 65
 66	protected function setCache($package, $key, $value) {
 67		return $this->cache->set('sly.package.'.$this->namespace, $this->getCacheKey($package, $key), $value);
 68	}
 69
 70	/**
 71	 * @param  string  $package       package name
 72	 * @param  boolean $forceRefresh  true to not use the cache and check if the composer.json is present
 73	 * @return boolean
 74	 */
 75	public function exists($package, $forceRefresh = false) {
 76		$exists = $forceRefresh ? null : $this->getCache($package, 'exists');
 77
 78		if ($exists === null) {
 79			$base   = $this->baseDirectory($package);
 80			$exists = file_exists($base.'composer.json');
 81
 82			$this->setCache($package, 'exists', $exists);
 83		}
 84
 85		return $exists;
 86	}
 87
 88	/**
 89	 * Get package author
 90	 *
 91	 * @param  string $package  package name
 92	 * @param  mixed  $default  default value if no author was specified in composer.json
 93	 * @return mixed            the author as given in static.yml
 94	 */
 95	public function getAuthor($package, $default = null) {
 96		$authors = $this->getKey($package, 'authors', null);
 97		if (!is_array($authors) || empty($authors)) return $default;
 98
 99		$first = reset($authors);
100		return isset($first['name']) ? $first['name'] : $default;
101	}
102
103	/**
104	 * Get support page
105	 *
106	 * @param  string $package  package name
107	 * @param  mixed  $default  default value if no page was specified in composer.json
108	 * @return mixed            the support page as given in static.yml
109	 */
110	public function getHomepage($package, $default = null) {
111		return $this->getKey($package, 'homepage', $default);
112	}
113
114	/**
115	 * Get parent package (only relevant for backend list)
116	 *
117	 * @param  string $package  package name
118	 * @param  mixed  $default  default value if no page was specified in composer.json
119	 * @return mixed            the parent package or null if not given
120	 */
121	public function getParent($package) {
122		return $this->getKey($package, 'parent', null);
123	}
124
125	/**
126	 * Get children packages (only relevant for backend list)
127	 *
128	 * @param  string $package  parent package name
129	 * @return array
130	 */
131	public function getChildren($parent) {
132		$packages = $this->getPackages();
133		$children = array();
134
135		foreach ($packages as $package) {
136			if ($this->getParent($package) === $parent) {
137				$children[] = $package;
138			}
139		}
140
141		return $children;
142	}
143
144	/**
145	 * Get version
146	 *
147	 * @param  string $package  package name
148	 * @param  mixed  $default  default value if no version was specified
149	 * @return string           the version
150	 */
151	public function getVersion($package, $default = null) {
152		return $this->getKey($package, 'version', $default);
153	}
154
155	/**
156	 * @param  string $package  package name
157	 * @return string           e.g. 'package/vendor:package'
158	 */
159	public function getVersionKey($package) {
160		return 'package/'.str_replace('/', ':', $package);
161	}
162
163	/**
164	 * Get last known version
165	 *
166	 * This method reads the last known version from the local config. This can
167	 * be used to determine whether a package has been updated.
168	 *
169	 * @param  string $package  package name
170	 * @param  mixed  $default  default value if no version was specified
171	 * @return string           the version
172	 */
173	public function getKnownVersion($package, $default = null) {
174		$key     = $this->getVersionKey($package);
175		$version = sly_Util_Versions::get($key);
176
177		return $version === false ? $default : $version;
178	}
179
180	/**
181	 * Set last known version
182	 *
183	 * @param  string $package  package name
184	 * @param  string $version  new version
185	 * @return string           the version
186	 */
187	public function setKnownVersion($package, $version) {
188		$key = $this->getVersionKey($package);
189		return sly_Util_Versions::set($key, $version);
190	}
191
192	/**
193	 * Read a config value from the composer.json
194	 *
195	 * @param  string $package  package name
196	 * @param  string $key      array key
197	 * @param  mixed  $default  value if key is not set
198	 * @return mixed            value or default
199	 */
200	public function getKey($package, $key, $default = null) {
201		if (!isset($this->composers[$package])) {
202			$composer = $this->getCache($package, 'composer.json');
203
204			if ($composer === null) {
205				$filename = $this->baseDirectory($package).'composer.json';
206				$composer = new sly_Util_Composer($filename);
207
208				$composer->setPackage($package);
209				$composer->getContent($this->baseDirectory().'composer'); // read file
210
211				$this->setCache($package, 'composer.json', $composer);
212			}
213			elseif (sly_Core::isDeveloperMode() && $composer->revalidate()) {
214				$this->setCache($package, 'composer.json', $composer);
215			}
216
217			$this->composers[$package] = $composer;
218		}
219
220		$value = $this->composers[$package]->getKey($key);
221		return $value === null ? $default : $value;
222	}
223
224	/**
225	 * Return a list of required packages
226	 *
227	 * Required packages are packages that $package itself needs to run.
228	 *
229	 * @param  string  $package    package name
230	 * @param  boolean $recursive  if true, requirements are search recursively
231	 * @param  array   $ignore     list of packages to ignore (and not recurse into)
232	 * @return array               list of required packages
233	 */
234	public function getRequirements($package, $recursive = true, array $ignore = array()) {
235		$cacheKey = 'requirements_'.($recursive ? 1 : 0);
236		$result   = $this->getCache($package, $cacheKey);
237
238		if ($result !== null) {
239			// apply ignore list
240			foreach ($ignore as $i) {
241				$idx = array_search($i, $result);
242				if ($idx !== false) unset($result[$idx]);
243			}
244
245			return array_values($result);
246		}
247
248		$stack  = array($package);
249		$stack  = array_merge($stack, array_keys($this->getKey($package, 'require', array())));
250		$result = array();
251
252		// don't add self
253		$ignore[] = $package;
254
255		do {
256			// take one out
257			$pkg = array_shift($stack);
258
259			// add its requirements
260			if ($this->exists($pkg) && $recursive) {
261				$stack = array_merge($stack, array_keys($this->getKey($pkg, 'require', array())));
262				$stack = array_unique($stack);
263			}
264
265			// filter out non-packages
266			foreach ($stack as $idx => $req) {
267				if (strpos($req, '/') === false) unset($stack[$idx]);
268			}
269
270			// respect ignore list
271			foreach ($ignore as $i) {
272				$idx = array_search($i, $stack);
273				if ($idx !== false) unset($stack[$idx]);
274			}
275
276			// do not add $package itself or duplicates
277			if ($pkg !== $package && !in_array($pkg, $result)) {
278				$result[] = $pkg;
279			}
280		}
281		while (!empty($stack));
282
283		natcasesort($result);
284		$this->setCache($package, $cacheKey, $result);
285
286		return $result;
287	}
288
289	/**
290	 * Return a list of dependent packages
291	 *
292	 * Dependent packages are packages that need $package to run.
293	 *
294	 * @param  string  $package    package name
295	 * @param  boolean $recursive  if true, dependencies are search recursively
296	 * @return array               list of required packages
297	 */
298	public function getDependencies($package, $recursive = true) {
299		$cacheKey = 'dependencies_'.($recursive ? 1 : 0);
300		$result   = $this->getCache($package, $cacheKey);
301
302		if ($result !== null) {
303			return $result;
304		}
305
306		$all    = $this->getPackages();
307		$stack  = array($package);
308		$result = array();
309
310		do {
311			// take one out
312			$pkg = array_shift($stack);
313
314			// find packages requiering $pkg
315			foreach ($all as $p) {
316				$requirements = array_keys($this->getKey($p, 'require', array()));
317
318				if (in_array($pkg, $requirements)) {
319					$result[] = $p;
320					$stack[]  = $p;
321				}
322			}
323
324			$stack = array_unique($stack);
325		}
326		while ($recursive && !empty($stack));
327
328		$result = array_unique($result);
329		natcasesort($result);
330		$this->setCache($package, $cacheKey, $result);
331
332		return $result;
333	}
334
335	/**
336	 * @return array  list of packages (cached if possible)
337	 */
338	public function getPackages() {
339		$packages = $this->getCache('', 'packages');
340
341		if ($packages === null || ($this->refreshed === false && sly_Core::isDeveloperMode())) {
342			$packages = $this->findPackages();
343
344			$this->refreshed = true;
345			$this->setCache('', 'packages', $packages);
346		}
347
348		return $packages;
349	}
350
351	/**
352	 * @return array  list of found packages
353	 */
354	public function findPackages() {
355		$root     = $this->baseDirectory();
356		$packages = array();
357
358		// If we're in a real composer vendor directory, there is a installed.json,
359		// that contains a list of all packages. We use this to detect packages
360		// have no composer.json themselves (aka leafo/lessphp).
361		// On the other hand, we must make sure that we only read those packages
362		// that are actually inside $root, as the installed.json will contain data
363		// about *all* packages (i.e. for vendors and addons)!
364		$installed = $root.'composer/installed.json';
365
366		if (file_exists($installed)) {
367			$data = sly_Util_JSON::load($installed);
368
369			foreach ($data as $pkg) {
370				if (is_dir($root.$pkg['name'])) {
371					$packages[] = $pkg['name'];
372				}
373			}
374
375			$installed = $root.'composer/installed_dev.json';
376
377			if (file_exists($installed)) {
378				$data = sly_Util_JSON::load($installed);
379
380				foreach ($data as $pkg) {
381					if (is_dir($root.$pkg['name'])) {
382						$packages[] = $pkg['name'];
383					}
384				}
385			}
386		}
387
388		// In addition to the installed.json, we should also scan the filesystem
389		// for valid packages. This makes it *much* easier to develop addOns
390		// and not modify and update your composer files over and over again.
391
392		$dirs = $this->readDir($root);
393
394		foreach ($dirs as $dir) {
395			// evil package not conforming to naming convention
396			if ($this->exists($dir, true)) {
397				$packages[] = $dir;
398			}
399			else {
400				$subdirs = $this->readDir($root.$dir);
401
402				foreach ($subdirs as $subdir) {
403					// good package
404					if ($this->exists($dir.'/'.$subdir, true)) {
405						$packages[] = $dir.'/'.$subdir;
406					}
407				}
408			}
409		}
410
411		natcasesort($packages);
412		return $packages;
413	}
414
415	private function readDir($dir) {
416		$dir = new sly_Util_Directory($dir);
417		return $dir->exists() ? $dir->listPlain(false, true) : array();
418	}
419}