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

Language PHP Lines 420
MD5 Hash f9df3df3a742c975413313ac949702c0 Estimated Cost $5,326 (why?)
Repository https://bitbucket.org/SallyCMS/trunk View Raw File
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
<?php
/*
 * Copyright (c) 2012, webvariants GbR, http://www.webvariants.de
 *
 * This file is released under the terms of the MIT license. You can find the
 * complete text in the attached LICENSE file or online at:
 *
 * http://www.opensource.org/licenses/mit-license.php
 */

/**
 * @author  christoph@webvariants.de
 * @ingroup service
 */
class sly_Service_Package {
	protected $sourceDir; ///< string
	protected $cache;     ///< BabelCache_Interface
	protected $composers; ///< array
	protected $refreshed; ///< boolean
	protected $namespace; ///< string

	/**
	 * @param string               $sourceDir
	 * @param BabelCache_Interface $cache
	 */
	public function __construct($sourceDir, BabelCache_Interface $cache) {
		$this->sourceDir = sly_Util_Directory::normalize($sourceDir).DIRECTORY_SEPARATOR;
		$this->cache     = $cache;
		$this->composers = array();
		$this->refreshed = false;
		$this->namespace = substr(md5($this->sourceDir), 0, 10).'_';
	}

	/**
	 * Clears the addOn metadata cache
	 */
	public function clearCache() {
		$this->cache->flush('sly.package', true);
		$this->composers = array();
		$this->refreshed = false;
	}

	/**
	 * @param  string $package   package name
	 * @return string
	 */
	public function baseDirectory($package = null) {
		$dir = $this->sourceDir;

		if (!empty($package)) {
			$dir .= str_replace('/', DIRECTORY_SEPARATOR, $package).DIRECTORY_SEPARATOR;
		}

		return $dir;
	}

	protected function getCacheKey($package, $key) {
		$package = str_replace('/', '%', $package);
		return ($package ? $package.'%' : '').$key;
	}

	protected function getCache($package, $key, $default = null) {
		return $this->cache->get('sly.package.'.$this->namespace, $this->getCacheKey($package, $key), $default);
	}

	protected function setCache($package, $key, $value) {
		return $this->cache->set('sly.package.'.$this->namespace, $this->getCacheKey($package, $key), $value);
	}

	/**
	 * @param  string  $package       package name
	 * @param  boolean $forceRefresh  true to not use the cache and check if the composer.json is present
	 * @return boolean
	 */
	public function exists($package, $forceRefresh = false) {
		$exists = $forceRefresh ? null : $this->getCache($package, 'exists');

		if ($exists === null) {
			$base   = $this->baseDirectory($package);
			$exists = file_exists($base.'composer.json');

			$this->setCache($package, 'exists', $exists);
		}

		return $exists;
	}

	/**
	 * Get package author
	 *
	 * @param  string $package  package name
	 * @param  mixed  $default  default value if no author was specified in composer.json
	 * @return mixed            the author as given in static.yml
	 */
	public function getAuthor($package, $default = null) {
		$authors = $this->getKey($package, 'authors', null);
		if (!is_array($authors) || empty($authors)) return $default;

		$first = reset($authors);
		return isset($first['name']) ? $first['name'] : $default;
	}

	/**
	 * Get support page
	 *
	 * @param  string $package  package name
	 * @param  mixed  $default  default value if no page was specified in composer.json
	 * @return mixed            the support page as given in static.yml
	 */
	public function getHomepage($package, $default = null) {
		return $this->getKey($package, 'homepage', $default);
	}

	/**
	 * Get parent package (only relevant for backend list)
	 *
	 * @param  string $package  package name
	 * @param  mixed  $default  default value if no page was specified in composer.json
	 * @return mixed            the parent package or null if not given
	 */
	public function getParent($package) {
		return $this->getKey($package, 'parent', null);
	}

	/**
	 * Get children packages (only relevant for backend list)
	 *
	 * @param  string $package  parent package name
	 * @return array
	 */
	public function getChildren($parent) {
		$packages = $this->getPackages();
		$children = array();

		foreach ($packages as $package) {
			if ($this->getParent($package) === $parent) {
				$children[] = $package;
			}
		}

		return $children;
	}

	/**
	 * Get version
	 *
	 * @param  string $package  package name
	 * @param  mixed  $default  default value if no version was specified
	 * @return string           the version
	 */
	public function getVersion($package, $default = null) {
		return $this->getKey($package, 'version', $default);
	}

	/**
	 * @param  string $package  package name
	 * @return string           e.g. 'package/vendor:package'
	 */
	public function getVersionKey($package) {
		return 'package/'.str_replace('/', ':', $package);
	}

	/**
	 * Get last known version
	 *
	 * This method reads the last known version from the local config. This can
	 * be used to determine whether a package has been updated.
	 *
	 * @param  string $package  package name
	 * @param  mixed  $default  default value if no version was specified
	 * @return string           the version
	 */
	public function getKnownVersion($package, $default = null) {
		$key     = $this->getVersionKey($package);
		$version = sly_Util_Versions::get($key);

		return $version === false ? $default : $version;
	}

	/**
	 * Set last known version
	 *
	 * @param  string $package  package name
	 * @param  string $version  new version
	 * @return string           the version
	 */
	public function setKnownVersion($package, $version) {
		$key = $this->getVersionKey($package);
		return sly_Util_Versions::set($key, $version);
	}

	/**
	 * Read a config value from the composer.json
	 *
	 * @param  string $package  package name
	 * @param  string $key      array key
	 * @param  mixed  $default  value if key is not set
	 * @return mixed            value or default
	 */
	public function getKey($package, $key, $default = null) {
		if (!isset($this->composers[$package])) {
			$composer = $this->getCache($package, 'composer.json');

			if ($composer === null) {
				$filename = $this->baseDirectory($package).'composer.json';
				$composer = new sly_Util_Composer($filename);

				$composer->setPackage($package);
				$composer->getContent($this->baseDirectory().'composer'); // read file

				$this->setCache($package, 'composer.json', $composer);
			}
			elseif (sly_Core::isDeveloperMode() && $composer->revalidate()) {
				$this->setCache($package, 'composer.json', $composer);
			}

			$this->composers[$package] = $composer;
		}

		$value = $this->composers[$package]->getKey($key);
		return $value === null ? $default : $value;
	}

	/**
	 * Return a list of required packages
	 *
	 * Required packages are packages that $package itself needs to run.
	 *
	 * @param  string  $package    package name
	 * @param  boolean $recursive  if true, requirements are search recursively
	 * @param  array   $ignore     list of packages to ignore (and not recurse into)
	 * @return array               list of required packages
	 */
	public function getRequirements($package, $recursive = true, array $ignore = array()) {
		$cacheKey = 'requirements_'.($recursive ? 1 : 0);
		$result   = $this->getCache($package, $cacheKey);

		if ($result !== null) {
			// apply ignore list
			foreach ($ignore as $i) {
				$idx = array_search($i, $result);
				if ($idx !== false) unset($result[$idx]);
			}

			return array_values($result);
		}

		$stack  = array($package);
		$stack  = array_merge($stack, array_keys($this->getKey($package, 'require', array())));
		$result = array();

		// don't add self
		$ignore[] = $package;

		do {
			// take one out
			$pkg = array_shift($stack);

			// add its requirements
			if ($this->exists($pkg) && $recursive) {
				$stack = array_merge($stack, array_keys($this->getKey($pkg, 'require', array())));
				$stack = array_unique($stack);
			}

			// filter out non-packages
			foreach ($stack as $idx => $req) {
				if (strpos($req, '/') === false) unset($stack[$idx]);
			}

			// respect ignore list
			foreach ($ignore as $i) {
				$idx = array_search($i, $stack);
				if ($idx !== false) unset($stack[$idx]);
			}

			// do not add $package itself or duplicates
			if ($pkg !== $package && !in_array($pkg, $result)) {
				$result[] = $pkg;
			}
		}
		while (!empty($stack));

		natcasesort($result);
		$this->setCache($package, $cacheKey, $result);

		return $result;
	}

	/**
	 * Return a list of dependent packages
	 *
	 * Dependent packages are packages that need $package to run.
	 *
	 * @param  string  $package    package name
	 * @param  boolean $recursive  if true, dependencies are search recursively
	 * @return array               list of required packages
	 */
	public function getDependencies($package, $recursive = true) {
		$cacheKey = 'dependencies_'.($recursive ? 1 : 0);
		$result   = $this->getCache($package, $cacheKey);

		if ($result !== null) {
			return $result;
		}

		$all    = $this->getPackages();
		$stack  = array($package);
		$result = array();

		do {
			// take one out
			$pkg = array_shift($stack);

			// find packages requiering $pkg
			foreach ($all as $p) {
				$requirements = array_keys($this->getKey($p, 'require', array()));

				if (in_array($pkg, $requirements)) {
					$result[] = $p;
					$stack[]  = $p;
				}
			}

			$stack = array_unique($stack);
		}
		while ($recursive && !empty($stack));

		$result = array_unique($result);
		natcasesort($result);
		$this->setCache($package, $cacheKey, $result);

		return $result;
	}

	/**
	 * @return array  list of packages (cached if possible)
	 */
	public function getPackages() {
		$packages = $this->getCache('', 'packages');

		if ($packages === null || ($this->refreshed === false && sly_Core::isDeveloperMode())) {
			$packages = $this->findPackages();

			$this->refreshed = true;
			$this->setCache('', 'packages', $packages);
		}

		return $packages;
	}

	/**
	 * @return array  list of found packages
	 */
	public function findPackages() {
		$root     = $this->baseDirectory();
		$packages = array();

		// If we're in a real composer vendor directory, there is a installed.json,
		// that contains a list of all packages. We use this to detect packages
		// have no composer.json themselves (aka leafo/lessphp).
		// On the other hand, we must make sure that we only read those packages
		// that are actually inside $root, as the installed.json will contain data
		// about *all* packages (i.e. for vendors and addons)!
		$installed = $root.'composer/installed.json';

		if (file_exists($installed)) {
			$data = sly_Util_JSON::load($installed);

			foreach ($data as $pkg) {
				if (is_dir($root.$pkg['name'])) {
					$packages[] = $pkg['name'];
				}
			}

			$installed = $root.'composer/installed_dev.json';

			if (file_exists($installed)) {
				$data = sly_Util_JSON::load($installed);

				foreach ($data as $pkg) {
					if (is_dir($root.$pkg['name'])) {
						$packages[] = $pkg['name'];
					}
				}
			}
		}

		// In addition to the installed.json, we should also scan the filesystem
		// for valid packages. This makes it *much* easier to develop addOns
		// and not modify and update your composer files over and over again.

		$dirs = $this->readDir($root);

		foreach ($dirs as $dir) {
			// evil package not conforming to naming convention
			if ($this->exists($dir, true)) {
				$packages[] = $dir;
			}
			else {
				$subdirs = $this->readDir($root.$dir);

				foreach ($subdirs as $subdir) {
					// good package
					if ($this->exists($dir.'/'.$subdir, true)) {
						$packages[] = $dir.'/'.$subdir;
					}
				}
			}
		}

		natcasesort($packages);
		return $packages;
	}

	private function readDir($dir) {
		$dir = new sly_Util_Directory($dir);
		return $dir->exists() ? $dir->listPlain(false, true) : array();
	}
}
Back to Top