PackageRepository.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. <?php
  2. /*
  3. * This file is part of Packagist.
  4. *
  5. * (c) Jordi Boggiano <j.boggiano@seld.be>
  6. * Nils Adermann <naderman@naderman.de>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Packagist\WebBundle\Entity;
  12. use Doctrine\ORM\EntityRepository;
  13. use Doctrine\ORM\QueryBuilder;
  14. use Doctrine\DBAL\Cache\QueryCacheProfile;
  15. /**
  16. * @author Jordi Boggiano <j.boggiano@seld.be>
  17. */
  18. class PackageRepository extends EntityRepository
  19. {
  20. public function findProviders($name)
  21. {
  22. $query = $this->createQueryBuilder('p')
  23. ->select('p')
  24. ->leftJoin('p.versions', 'pv')
  25. ->leftJoin('pv.provide', 'pr')
  26. ->where('pv.development = true')
  27. ->andWhere('pr.packageName = :name')
  28. ->orderBy('p.name')
  29. ->getQuery()
  30. ->setParameters(array('name' => $name));
  31. return $query->getResult();
  32. }
  33. public function getPackageNamesUpdatedSince(\DateTimeInterface $date)
  34. {
  35. $query = $this->getEntityManager()
  36. ->createQuery("
  37. SELECT p.name FROM Packagist\WebBundle\Entity\Package p
  38. WHERE p.dumpedAt >= :date AND (p.replacementPackage IS NULL OR p.replacementPackage != 'spam/spam')
  39. ")
  40. ->setParameters(['date' => $date]);
  41. $names = $this->getPackageNamesForQuery($query);
  42. return array_map('strtolower', $names);
  43. }
  44. public function getPackageNames()
  45. {
  46. $query = $this->getEntityManager()
  47. ->createQuery("SELECT p.name FROM Packagist\WebBundle\Entity\Package p");
  48. $names = $this->getPackageNamesForQuery($query);
  49. return array_map('strtolower', $names);
  50. }
  51. public function getProvidedNames()
  52. {
  53. $query = $this->getEntityManager()
  54. ->createQuery("SELECT p.packageName AS name
  55. FROM Packagist\WebBundle\Entity\ProvideLink p
  56. LEFT JOIN p.version v
  57. WHERE v.development = true
  58. GROUP BY p.packageName");
  59. $names = $this->getPackageNamesForQuery($query);
  60. return array_map('strtolower', $names);
  61. }
  62. public function getPackageNamesByType($type)
  63. {
  64. $query = $this->getEntityManager()
  65. ->createQuery("SELECT p.name FROM Packagist\WebBundle\Entity\Package p WHERE p.type = :type AND (p.replacementPackage IS NULL OR p.replacementPackage != 'spam/spam')")
  66. ->setParameters(array('type' => $type));
  67. return $this->getPackageNamesForQuery($query);
  68. }
  69. public function getPackageNamesByVendor($vendor)
  70. {
  71. $query = $this->getEntityManager()
  72. ->createQuery("SELECT p.name FROM Packagist\WebBundle\Entity\Package p WHERE p.name LIKE :vendor AND (p.replacementPackage IS NULL OR p.replacementPackage != 'spam/spam')")
  73. ->setParameters(array('vendor' => $vendor.'/%'));
  74. return $this->getPackageNamesForQuery($query);
  75. }
  76. public function getGitHubPackagesByMaintainer(int $userId)
  77. {
  78. $query = $this->createQueryBuilder('p')
  79. ->select('p')
  80. ->leftJoin('p.maintainers', 'm')
  81. ->where('m.id = :userId')
  82. ->andWhere('p.repository LIKE :repoUrl')
  83. ->orderBy('p.autoUpdated', 'ASC')
  84. ->getQuery()
  85. ->setParameters(['userId' => $userId, 'repoUrl' => 'https://github.com/%']);
  86. return $query->getResult();
  87. }
  88. public function getPackagesWithFields($filters, $fields)
  89. {
  90. $selector = '';
  91. foreach ($fields as $field) {
  92. $selector .= ', p.'.$field;
  93. }
  94. $where = 'p.replacementPackage != :replacement';
  95. foreach ($filters as $filter => $val) {
  96. $where .= ' AND p.'.$filter.' = :'.$filter;
  97. }
  98. $filters['replacement'] = "spam/spam";
  99. $query = $this->getEntityManager()
  100. ->createQuery("SELECT p.name $selector FROM Packagist\WebBundle\Entity\Package p WHERE $where")
  101. ->setParameters($filters);
  102. $result = array();
  103. foreach ($query->getScalarResult() as $row) {
  104. $name = $row['name'];
  105. unset($row['name']);
  106. $result[$name] = $row;
  107. }
  108. return $result;
  109. }
  110. private function getPackageNamesForQuery($query)
  111. {
  112. $names = array();
  113. foreach ($query->getScalarResult() as $row) {
  114. $names[] = $row['name'];
  115. }
  116. if (defined('SORT_FLAG_CASE')) {
  117. sort($names, SORT_STRING | SORT_FLAG_CASE);
  118. } else {
  119. sort($names, SORT_STRING);
  120. }
  121. return $names;
  122. }
  123. public function getStalePackages()
  124. {
  125. $conn = $this->getEntityManager()->getConnection();
  126. return $conn->fetchAll(
  127. 'SELECT p.id FROM package p
  128. WHERE p.abandoned = false
  129. AND (
  130. p.crawledAt IS NULL
  131. OR (p.autoUpdated = 0 AND p.crawledAt < :recent AND p.createdAt >= :yesterday)
  132. OR (p.autoUpdated = 0 AND p.crawledAt < :crawled)
  133. OR (p.crawledAt < :autocrawled)
  134. )
  135. ORDER BY p.id ASC',
  136. array(
  137. // crawl new packages every 3h for the first day so that dummy packages get deleted ASAP
  138. 'recent' => date('Y-m-d H:i:s', strtotime('-3hour')),
  139. 'yesterday' => date('Y-m-d H:i:s', strtotime('-1day')),
  140. // crawl packages without auto-update once every 2week
  141. 'crawled' => date('Y-m-d H:i:s', strtotime('-2week')),
  142. // crawl all packages including auto-updated once a month just in case
  143. 'autocrawled' => date('Y-m-d H:i:s', strtotime('-1month')),
  144. )
  145. );
  146. }
  147. public function getStalePackagesForIndexing()
  148. {
  149. $conn = $this->getEntityManager()->getConnection();
  150. return $conn->fetchAll('SELECT p.id FROM package p WHERE p.indexedAt IS NULL OR p.indexedAt <= p.crawledAt ORDER BY p.id ASC');
  151. }
  152. public function getStalePackagesForDumping()
  153. {
  154. $conn = $this->getEntityManager()->getConnection();
  155. return $conn->fetchAll('SELECT p.id FROM package p WHERE p.dumpedAt IS NULL OR p.dumpedAt <= p.crawledAt AND p.crawledAt < NOW() ORDER BY p.id ASC');
  156. }
  157. public function iterateStaleDownloadCountPackageIds()
  158. {
  159. $qb = $this->createQueryBuilder('p');
  160. $res = $qb
  161. ->select('p.id, d.lastUpdated, p.createdAt')
  162. ->leftJoin('p.downloads', 'd')
  163. ->where('((d.type = :type AND d.lastUpdated < :time) OR d.lastUpdated IS NULL)')
  164. ->setParameters(['type' => Download::TYPE_PACKAGE, 'time' => new \DateTime('-20hours')])
  165. ->getQuery()
  166. ->getResult();
  167. foreach ($res as $row) {
  168. yield ['id' => $row['id'], 'lastUpdated' => is_null($row['lastUpdated']) ? new \DateTimeImmutable($row['createdAt']->format('r')) : new \DateTimeImmutable($row['lastUpdated']->format('r'))];
  169. }
  170. }
  171. public function getPartialPackageByNameWithVersions($name)
  172. {
  173. // first fetch a partial package including joined versions/maintainers, that way
  174. // the join is cheap and heavy data (description, readme) is not duplicated for each joined row
  175. //
  176. // fetching everything partial here to avoid fetching tons of data,
  177. // this helps for packages like https://packagist.org/packages/ccxt/ccxt
  178. // with huge amounts of versions
  179. $qb = $this->getEntityManager()->createQueryBuilder();
  180. $qb->select('partial p.{id}', 'partial v.{id, version, normalizedVersion, development, releasedAt}', 'partial m.{id, username, email}')
  181. ->from('Packagist\WebBundle\Entity\Package', 'p')
  182. ->leftJoin('p.versions', 'v')
  183. ->leftJoin('p.maintainers', 'm')
  184. ->orderBy('v.development', 'DESC')
  185. ->addOrderBy('v.releasedAt', 'DESC')
  186. ->where('p.name = ?0')
  187. ->setParameters(array($name));
  188. $pkg = $qb->getQuery()->getSingleResult();
  189. if ($pkg) {
  190. // then refresh the package to complete its data and inject the previously fetched versions/maintainers to
  191. // get a complete package
  192. $versions = $pkg->getVersions();
  193. $maintainers = $pkg->getMaintainers();
  194. $this->getEntityManager()->refresh($pkg);
  195. $prop = new \ReflectionProperty($pkg, 'versions');
  196. $prop->setAccessible(true);
  197. $prop->setValue($pkg, $versions);
  198. $prop = new \ReflectionProperty($pkg, 'maintainers');
  199. $prop->setAccessible(true);
  200. $prop->setValue($pkg, $maintainers);
  201. }
  202. return $pkg;
  203. }
  204. public function getPackageByName($name)
  205. {
  206. $qb = $this->getEntityManager()->createQueryBuilder();
  207. $qb->select('p', 'm')
  208. ->from('Packagist\WebBundle\Entity\Package', 'p')
  209. ->leftJoin('p.maintainers', 'm')
  210. ->where('p.name = ?0')
  211. ->setParameters(array($name));
  212. return $qb->getQuery()->getSingleResult();
  213. }
  214. public function getPackagesWithVersions(array $ids = null, $filters = array())
  215. {
  216. $qb = $this->getEntityManager()->createQueryBuilder();
  217. $qb->select('p', 'v')
  218. ->from('Packagist\WebBundle\Entity\Package', 'p')
  219. ->leftJoin('p.versions', 'v')
  220. ->orderBy('v.development', 'DESC')
  221. ->addOrderBy('v.releasedAt', 'DESC');
  222. if (null !== $ids) {
  223. $qb->where($qb->expr()->in('p.id', ':ids'))
  224. ->setParameter('ids', $ids);
  225. }
  226. $this->addFilters($qb, $filters);
  227. return $qb->getQuery()->getResult();
  228. }
  229. public function getGitHubStars(array $ids)
  230. {
  231. $qb = $this->getEntityManager()->createQueryBuilder();
  232. $qb->select('p.gitHubStars', 'p.id')
  233. ->from('Packagist\WebBundle\Entity\Package', 'p')
  234. ->where($qb->expr()->in('p.id', ':ids'))
  235. ->setParameter('ids', $ids);
  236. return $qb->getQuery()->getResult();
  237. }
  238. public function getFilteredQueryBuilder(array $filters = array(), $orderByName = false)
  239. {
  240. $qb = $this->getEntityManager()->createQueryBuilder();
  241. $qb->select('p')
  242. ->from('Packagist\WebBundle\Entity\Package', 'p');
  243. if (isset($filters['tag'])) {
  244. $qb->leftJoin('p.versions', 'v');
  245. $qb->leftJoin('v.tags', 't');
  246. }
  247. $qb->orderBy('p.abandoned');
  248. if (true === $orderByName) {
  249. $qb->addOrderBy('p.name');
  250. } else {
  251. $qb->addOrderBy('p.id', 'DESC');
  252. }
  253. $this->addFilters($qb, $filters);
  254. return $qb;
  255. }
  256. public function isVendorTaken($vendor, User $user)
  257. {
  258. $query = $this->getEntityManager()
  259. ->createQuery(
  260. "SELECT p.name, m.id user_id
  261. FROM Packagist\WebBundle\Entity\Package p
  262. JOIN p.maintainers m
  263. WHERE p.name LIKE :vendor")
  264. ->setParameters(array('vendor' => $vendor.'/%'));
  265. $rows = $query->getArrayResult();
  266. if (!$rows) {
  267. return false;
  268. }
  269. foreach ($rows as $row) {
  270. if ($row['user_id'] === $user->getId()) {
  271. return false;
  272. }
  273. }
  274. return true;
  275. }
  276. public function getDependentCount($name)
  277. {
  278. $sql = 'SELECT COUNT(*) count FROM (
  279. SELECT pv.package_id FROM link_require r INNER JOIN package_version pv ON (pv.id = r.version_id AND pv.development = 1) WHERE r.packageName = :name
  280. UNION
  281. SELECT pv.package_id FROM link_require_dev r INNER JOIN package_version pv ON (pv.id = r.version_id AND pv.development = 1) WHERE r.packageName = :name
  282. ) x';
  283. $stmt = $this->getEntityManager()->getConnection()
  284. ->executeCacheQuery($sql, ['name' => $name], [], new QueryCacheProfile(7*86400, 'dependents_count_'.$name, $this->getEntityManager()->getConfiguration()->getResultCacheImpl()));
  285. $result = $stmt->fetchAll();
  286. $stmt->closeCursor();
  287. return (int) $result[0]['count'];
  288. }
  289. public function getDependents($name, $offset = 0, $limit = 15)
  290. {
  291. $sql = 'SELECT p.id, p.name, p.description, p.language, p.abandoned, p.replacementPackage
  292. FROM package p INNER JOIN (
  293. SELECT pv.package_id FROM link_require r INNER JOIN package_version pv ON (pv.id = r.version_id AND pv.development = 1) WHERE r.packageName = :name
  294. UNION
  295. SELECT pv.package_id FROM link_require_dev r INNER JOIN package_version pv ON (pv.id = r.version_id AND pv.development = 1) WHERE r.packageName = :name
  296. ) x ON x.package_id = p.id ORDER BY p.name ASC LIMIT '.((int)$limit).' OFFSET '.((int)$offset);
  297. $stmt = $this->getEntityManager()->getConnection()
  298. ->executeCacheQuery(
  299. $sql,
  300. ['name' => $name],
  301. [],
  302. new QueryCacheProfile(7*86400, 'dependents_'.$name.'_'.$offset.'_'.$limit, $this->getEntityManager()->getConfiguration()->getResultCacheImpl())
  303. );
  304. $result = $stmt->fetchAll();
  305. $stmt->closeCursor();
  306. return $result;
  307. }
  308. public function getSuggestCount($name)
  309. {
  310. $sql = 'SELECT COUNT(DISTINCT pv.package_id) count
  311. FROM link_suggest s
  312. INNER JOIN package_version pv ON (pv.id = s.version_id AND pv.development = 1)
  313. WHERE s.packageName = :name';
  314. $stmt = $this->getEntityManager()->getConnection()
  315. ->executeCacheQuery($sql, ['name' => $name], [], new QueryCacheProfile(7*86400, 'suggesters_count_'.$name, $this->getEntityManager()->getConfiguration()->getResultCacheImpl()));
  316. $result = $stmt->fetchAll();
  317. $stmt->closeCursor();
  318. return (int) $result[0]['count'];
  319. }
  320. public function getSuggests($name, $offset = 0, $limit = 15)
  321. {
  322. $sql = 'SELECT p.id, p.name, p.description, p.language, p.abandoned, p.replacementPackage
  323. FROM link_suggest s
  324. INNER JOIN package_version pv ON (pv.id = s.version_id AND pv.development = 1)
  325. INNER JOIN package p ON (p.id = pv.package_id)
  326. WHERE s.packageName = :name
  327. GROUP BY pv.package_id
  328. ORDER BY p.name ASC LIMIT '.((int)$limit).' OFFSET '.((int)$offset);
  329. $stmt = $this->getEntityManager()->getConnection()
  330. ->executeCacheQuery(
  331. $sql,
  332. ['name' => $name],
  333. [],
  334. new QueryCacheProfile(7*86400, 'suggesters_'.$name.'_'.$offset.'_'.$limit, $this->getEntityManager()->getConfiguration()->getResultCacheImpl())
  335. );
  336. $result = $stmt->fetchAll();
  337. $stmt->closeCursor();
  338. return $result;
  339. }
  340. private function addFilters(QueryBuilder $qb, array $filters)
  341. {
  342. foreach ($filters as $name => $value) {
  343. if (null === $value) {
  344. continue;
  345. }
  346. switch ($name) {
  347. case 'tag':
  348. $qb->andWhere($qb->expr()->in('t.name', ':'.$name));
  349. break;
  350. case 'maintainer':
  351. $qb->leftJoin('p.maintainers', 'm');
  352. $qb->andWhere($qb->expr()->in('m.id', ':'.$name));
  353. break;
  354. case 'vendor':
  355. $qb->andWhere('p.name LIKE :vendor');
  356. break;
  357. default:
  358. $qb->andWhere($qb->expr()->in('p.'.$name, ':'.$name));
  359. break;
  360. }
  361. $qb->setParameter($name, $value);
  362. }
  363. }
  364. /**
  365. * Gets the most recent packages created
  366. *
  367. * @return QueryBuilder
  368. */
  369. public function getQueryBuilderForNewestPackages()
  370. {
  371. $qb = $this->getEntityManager()->createQueryBuilder();
  372. $qb->select('p')
  373. ->from('Packagist\WebBundle\Entity\Package', 'p')
  374. ->where('p.abandoned = false')
  375. ->orderBy('p.id', 'DESC');
  376. return $qb;
  377. }
  378. }