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