From 697b80aaf0e9781671acba989fe1ea0904c22fa7 Mon Sep 17 00:00:00 2001
From: Ayoub Kaanich <kayoub5@live.com>
Date: Fri, 8 Jun 2018 06:15:13 +0200
Subject: [PATCH] feat: composer exact semver support (without lockfile
 updates) (#1993)

This PR adds the packagist datasource plus basic exact semver Composer support. Composer lockfile updating is not yet supported.
---
 lib/config/definitions.js                     |  21 ++
 lib/datasource/packagist.js                   |  57 ++++
 lib/manager/composer/extract.js               |  51 ++++
 lib/manager/composer/index.js                 |  12 +
 lib/manager/index.js                          |   1 +
 .../repository/process/lookup/index.js        |   3 +
 test/_fixtures/composer/composer1.json        |  74 +++++
 test/_fixtures/packagist/uploader.json        |   1 +
 .../__snapshots__/packagist.spec.js.snap      |  43 +++
 test/datasource/packagist.spec.js             |  38 +++
 .../__snapshots__/extract.spec.js.snap        | 253 ++++++++++++++++++
 test/manager/composer/extract.spec.js         |  28 ++
 .../extract/__snapshots__/index.spec.js.snap  |   3 +
 .../lookup/__snapshots__/index.spec.js.snap   |   9 +
 .../repository/process/lookup/index.spec.js   |  10 +
 website/docs/configuration-options.md         |   8 +
 16 files changed, 612 insertions(+)
 create mode 100644 lib/datasource/packagist.js
 create mode 100644 lib/manager/composer/extract.js
 create mode 100644 lib/manager/composer/index.js
 create mode 100644 test/_fixtures/composer/composer1.json
 create mode 100644 test/_fixtures/packagist/uploader.json
 create mode 100644 test/datasource/__snapshots__/packagist.spec.js.snap
 create mode 100644 test/datasource/packagist.spec.js
 create mode 100644 test/manager/composer/__snapshots__/extract.spec.js.snap
 create mode 100644 test/manager/composer/extract.spec.js

diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index 432609647d..df8d53ebe0 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -958,6 +958,27 @@ const options = [
     mergeable: true,
     cli: false,
   },
+  {
+    name: 'composer',
+    description: 'Configuration object for composer.json files',
+    stage: 'package',
+    type: 'json',
+    default: {
+      enabled: false,
+      fileMatch: ['(^|\\/)([\\w-]*)composer.json$'],
+    },
+    mergeable: true,
+    cli: false,
+  },
+  {
+    name: 'php',
+    description: 'Configuration object for php',
+    stage: 'package',
+    type: 'json',
+    default: {},
+    mergeable: true,
+    cli: false,
+  },
   {
     name: 'pip_requirements',
     description: 'Configuration object for requirements.txt files',
diff --git a/lib/datasource/packagist.js b/lib/datasource/packagist.js
new file mode 100644
index 0000000000..3f52a24062
--- /dev/null
+++ b/lib/datasource/packagist.js
@@ -0,0 +1,57 @@
+const URL = require('url');
+const got = require('got');
+const parse = require('github-url-from-git');
+const semver = require('semver');
+
+module.exports = {
+  getDependency,
+};
+
+async function getDependency(name) {
+  logger.trace(`getDependency(${name})`);
+
+  const regUrl = 'https://packagist.org';
+
+  const pkgUrl = URL.resolve(regUrl, `/packages/${name}.json`);
+
+  try {
+    const res = (await got(pkgUrl, {
+      json: true,
+      retries: 5,
+    })).body.package;
+
+    // Simplify response before caching and returning
+    const dep = {
+      name: res.name,
+      versions: {},
+    };
+
+    if (res.repository) {
+      dep.repositoryUrl = parse(res.repository);
+    }
+
+    Object.keys(res.versions)
+      .filter(version => semver.valid(version))
+      .forEach(version => {
+        const release = res.versions[version];
+        dep.homepage = dep.homepage || release.homepage;
+        dep.versions[semver.valid(version)] = {
+          gitHead: version,
+          time: release.time,
+        };
+      });
+    dep.homepage = dep.homepage || res.repository;
+    logger.trace({ dep }, 'dep');
+    return dep;
+  } catch (err) {
+    if (err.statusCode === 404 || err.code === 'ENOTFOUND') {
+      logger.info({ name }, `Dependency lookup failure: not found`);
+      logger.debug({
+        err,
+      });
+      return null;
+    }
+    logger.warn({ err, name }, 'packagist registry failure: Unknown error');
+    return null;
+  }
+}
diff --git a/lib/manager/composer/extract.js b/lib/manager/composer/extract.js
new file mode 100644
index 0000000000..aa54acefa2
--- /dev/null
+++ b/lib/manager/composer/extract.js
@@ -0,0 +1,51 @@
+const semver = require('../../versioning')('semver');
+
+module.exports = {
+  extractDependencies,
+};
+
+function extractDependencies(content, packageFile) {
+  logger.debug('composer.extractDependencies()');
+  let packageJson;
+  try {
+    packageJson = JSON.parse(content);
+  } catch (err) {
+    logger.info({ packageFile }, 'Invalid JSON');
+    return null;
+  }
+  const deps = [];
+  const depTypes = ['require', 'require-dev'];
+  for (const depType of depTypes) {
+    if (packageJson[depType]) {
+      try {
+        for (const [depName, version] of Object.entries(packageJson[depType])) {
+          const currentValue = version.trim();
+          const dep = {
+            depType,
+            depName,
+            currentValue,
+            verionScheme: 'semver',
+            purl: 'pkg:packagist/' + depName,
+          };
+          if (!depName.includes('/')) {
+            dep.skipReason = 'unsupported';
+          }
+          if (!semver.isVersion(currentValue)) {
+            dep.skipReason = 'unsupported-constraint';
+          }
+          deps.push(dep);
+        }
+      } catch (err) /* istanbul ignore next */ {
+        logger.info(
+          { packageFile, depType, err, message: err.message },
+          'Error parsing composer.json'
+        );
+        return null;
+      }
+    }
+  }
+  if (!deps.length) {
+    return null;
+  }
+  return { deps };
+}
diff --git a/lib/manager/composer/index.js b/lib/manager/composer/index.js
new file mode 100644
index 0000000000..9fd44f840d
--- /dev/null
+++ b/lib/manager/composer/index.js
@@ -0,0 +1,12 @@
+const { extractDependencies } = require('./extract');
+const { updateDependency } = require('../npm/update');
+
+const language = 'php';
+
+module.exports = {
+  extractDependencies,
+  language,
+  updateDependency,
+  // TODO: support this
+  // supportsLockFileMaintenance: true,
+};
diff --git a/lib/manager/index.js b/lib/manager/index.js
index d4b38352cd..236458e779 100644
--- a/lib/manager/index.js
+++ b/lib/manager/index.js
@@ -2,6 +2,7 @@ const managerList = [
   'bazel',
   'buildkite',
   'circleci',
+  'composer',
   'docker',
   'docker-compose',
   'meteor',
diff --git a/lib/workers/repository/process/lookup/index.js b/lib/workers/repository/process/lookup/index.js
index ef58b90c34..0c905b0dc3 100644
--- a/lib/workers/repository/process/lookup/index.js
+++ b/lib/workers/repository/process/lookup/index.js
@@ -4,6 +4,7 @@ const { getRangeStrategy } = require('../../../../manager');
 const { filterVersions } = require('./filter');
 const npmApi = require('../../../../datasource/npm');
 const github = require('../../../../datasource/github');
+const packagist = require('../../../../datasource/packagist');
 const pypi = require('../../../../datasource/pypi');
 const { parse } = require('../../../../../lib/util/purl');
 
@@ -40,6 +41,8 @@ async function lookupUpdates(config) {
     dependency = await github.getDependency(purl.fullname, purl.qualifiers);
   } else if (purl.type === 'pypi') {
     dependency = await pypi.getDependency(purl.fullname);
+  } else if (purl.type === 'packagist') {
+    dependency = await packagist.getDependency(purl.fullname);
   } else {
     logger.warn({ config }, 'Unknown purl');
     return [];
diff --git a/test/_fixtures/composer/composer1.json b/test/_fixtures/composer/composer1.json
new file mode 100644
index 0000000000..84c31421b7
--- /dev/null
+++ b/test/_fixtures/composer/composer1.json
@@ -0,0 +1,74 @@
+{
+  "autoload": {
+      "psr-0": {
+          "": "src/"
+      }
+  },
+  "require": {
+      "php": ">=5.3.2",
+
+      "symfony/assetic-bundle": "dev-master",
+      "symfony/monolog-bundle": "dev-master",
+      "symfony/swiftmailer-bundle": "dev-master",
+      "symfony/symfony": "2.1.*",
+
+      "doctrine/common": "2.2.2",
+      "doctrine/doctrine-bundle": "dev-master",
+      "doctrine/doctrine-fixtures-bundle": "dev-master",
+      "doctrine/orm": "2.2.x-dev",
+
+      "exercise/elastica-bundle": "dev-master",
+
+      "friendsofsymfony/rest-bundle": "dev-master",
+      "friendsofsymfony/user-bundle": "*",
+
+      "fzaninotto/faker": "*",
+
+      "jms/di-extra-bundle": "1.0.1",
+      "jms/payment-core-bundle": "*",
+      "jms/security-extra-bundle": "1.1.0",
+
+      "knplabs/knp-menu-bundle": "dev-master",
+      "knplabs/knp-paginator-bundle": "dev-master",
+
+      "liip/imagine-bundle": "dev-master",
+
+      "merk/dough-bundle": "dev-master",
+
+      "sensio/distribution-bundle": "dev-master",
+      "sensio/framework-extra-bundle": "dev-master",
+      "sensio/generator-bundle": "dev-master",
+
+      "simplethings/entity-audit-bundle": "dev-master",
+
+      "stof/doctrine-extensions-bundle": "dev-master",
+
+      "twig/extensions": "dev-master"
+  },
+  "require-dev": {
+      "behat/behat": "2.3.*",
+      "behat/behat-bundle": "*",
+      "behat/mink-bundle": "*",
+      "behat/sahi-client": "*",
+      "behat/common-contexts": "*"
+  },
+  "scripts": {
+      "post-install-cmd": [
+          "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
+          "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
+          "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets"
+      ],
+      "post-update-cmd": [
+          "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
+          "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
+          "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets"
+      ]
+  },
+  "config": {
+      "bin-dir": "bin"
+  },
+  "extra": {
+      "symfony-app-dir": "app",
+      "symfony-web-dir": "web"
+  }
+}
diff --git a/test/_fixtures/packagist/uploader.json b/test/_fixtures/packagist/uploader.json
new file mode 100644
index 0000000000..2334744c0b
--- /dev/null
+++ b/test/_fixtures/packagist/uploader.json
@@ -0,0 +1 @@
+{"package":{"name":"cristianvuolo\/uploader","description":"Addon Uploader","time":"2016-10-19T22:49:42+00:00","maintainers":[{"name":"cristianvuolo","avatar_url":"https:\/\/www.gravatar.com\/avatar\/ed04ec5b11a6c9b660018342d6fcdc1a?d=identicon"}],"versions":{"dev-master":{"name":"cristianvuolo\/uploader","description":"Addon Uploader","keywords":[],"homepage":"","version":"dev-master","version_normalized":"9999999-dev","license":["MIT"],"authors":[],"source":{"type":"git","url":"https:\/\/github.com\/cristianvuolo\/uploader.git","reference":"fa3a834d521326794b920a6f983979d3a8c635f2"},"dist":{"type":"zip","url":"https:\/\/api.github.com\/repos\/cristianvuolo\/uploader\/zipball\/fa3a834d521326794b920a6f983979d3a8c635f2","reference":"fa3a834d521326794b920a6f983979d3a8c635f2","shasum":""},"type":"library","time":"2018-03-14T17:40:06+00:00","autoload":{"psr-4":{"CristianVuolo\\Uploader\\":"src\/"}},"require":{"php":"\u003E=5.6.4","intervention\/image":"^2.3","laravel\/framework":"5.*"}},"1.0.8":{"name":"cristianvuolo\/uploader","description":"Addon Uploader","keywords":[],"homepage":"","version":"1.0.8","version_normalized":"1.0.8.0","license":["MIT"],"authors":[],"source":{"type":"git","url":"https:\/\/github.com\/cristianvuolo\/uploader.git","reference":"fa3a834d521326794b920a6f983979d3a8c635f2"},"dist":{"type":"zip","url":"https:\/\/api.github.com\/repos\/cristianvuolo\/uploader\/zipball\/fa3a834d521326794b920a6f983979d3a8c635f2","reference":"fa3a834d521326794b920a6f983979d3a8c635f2","shasum":""},"type":"library","time":"2018-03-14T17:40:06+00:00","autoload":{"psr-4":{"CristianVuolo\\Uploader\\":"src\/"}},"require":{"php":"\u003E=5.6.4","intervention\/image":"^2.3","laravel\/framework":"5.*"}},"1.0.7":{"name":"cristianvuolo\/uploader","description":"Addon Uploader","keywords":[],"homepage":"","version":"1.0.7","version_normalized":"1.0.7.0","license":["MIT"],"authors":[],"source":{"type":"git","url":"https:\/\/github.com\/cristianvuolo\/uploader.git","reference":"d71a2e83673151f1de632ca656541c063da9b9f6"},"dist":{"type":"zip","url":"https:\/\/api.github.com\/repos\/cristianvuolo\/uploader\/zipball\/d71a2e83673151f1de632ca656541c063da9b9f6","reference":"d71a2e83673151f1de632ca656541c063da9b9f6","shasum":""},"type":"library","time":"2018-02-19T12:22:34+00:00","autoload":{"psr-4":{"CristianVuolo\\Uploader\\":"src\/"}},"require":{"php":"\u003E=5.6.4","intervention\/image":"^2.3","laravel\/framework":"5.*"}},"1.0.6":{"name":"cristianvuolo\/uploader","description":"","keywords":[],"homepage":"","version":"1.0.6","version_normalized":"1.0.6.0","license":["MIT"],"authors":[],"source":{"type":"git","url":"https:\/\/github.com\/cristianvuolo\/uploader.git","reference":"b9b4b7049976525018a45ef6b18c1575b56ddd10"},"dist":{"type":"zip","url":"https:\/\/api.github.com\/repos\/cristianvuolo\/uploader\/zipball\/b9b4b7049976525018a45ef6b18c1575b56ddd10","reference":"b9b4b7049976525018a45ef6b18c1575b56ddd10","shasum":""},"type":"library","time":"2018-01-25T18:57:17+00:00","autoload":{"psr-4":{"CristianVuolo\\Uploader\\":"src\/"}},"require":{"php":"\u003E=5.6.4","intervention\/image":"^2.3","laravel\/framework":"5.*"}},"1.0.5":{"name":"cristianvuolo\/uploader","description":"","keywords":[],"homepage":"","version":"1.0.5","version_normalized":"1.0.5.0","license":["MIT"],"authors":[],"source":{"type":"git","url":"https:\/\/github.com\/cristianvuolo\/uploader.git","reference":"5c346fc2e8bdae20522cb880b64112e2b380e064"},"dist":{"type":"zip","url":"https:\/\/api.github.com\/repos\/cristianvuolo\/uploader\/zipball\/5c346fc2e8bdae20522cb880b64112e2b380e064","reference":"5c346fc2e8bdae20522cb880b64112e2b380e064","shasum":""},"type":"library","time":"2017-08-02T13:23:33+00:00","autoload":{"psr-4":{"CristianVuolo\\Uploader\\":"src\/"}},"require":{"php":"\u003E=5.6.4","intervention\/image":"^2.3","laravel\/framework":"5.*"}},"1.0.4":{"name":"cristianvuolo\/uploader","description":"","keywords":[],"homepage":"","version":"1.0.4","version_normalized":"1.0.4.0","license":["MIT"],"authors":[],"source":{"type":"git","url":"https:\/\/github.com\/cristianvuolo\/uploader.git","reference":"7cbe0c930997743ba4d86147526358784575b79e"},"dist":{"type":"zip","url":"https:\/\/api.github.com\/repos\/cristianvuolo\/uploader\/zipball\/7cbe0c930997743ba4d86147526358784575b79e","reference":"7cbe0c930997743ba4d86147526358784575b79e","shasum":""},"type":"library","time":"2017-08-02T13:08:31+00:00","autoload":{"psr-4":{"CristianVuolo\\Uploader\\":"src\/"}},"require":{"php":"\u003E=5.6.4","intervention\/image":"^2.3","laravel\/framework":"5.*"}},"1.0.3":{"name":"cristianvuolo\/uploader","description":"","keywords":[],"homepage":"","version":"1.0.3","version_normalized":"1.0.3.0","license":["MIT"],"authors":[],"source":{"type":"git","url":"https:\/\/github.com\/cristianvuolo\/uploader.git","reference":"77b94732179a4821063fb4dc6a68b5af6e67a94a"},"dist":{"type":"zip","url":"https:\/\/api.github.com\/repos\/cristianvuolo\/uploader\/zipball\/77b94732179a4821063fb4dc6a68b5af6e67a94a","reference":"77b94732179a4821063fb4dc6a68b5af6e67a94a","shasum":""},"type":"library","time":"2017-07-11T16:52:54+00:00","autoload":{"psr-4":{"CristianVuolo\\Uploader\\":"src\/"}},"require":{"php":"\u003E=5.6.4","intervention\/image":"^2.3","laravel\/framework":"5.*"}},"1.0.1":{"name":"cristianvuolo\/uploader","description":"","keywords":[],"homepage":"","version":"1.0.1","version_normalized":"1.0.1.0","license":["MIT"],"authors":[],"source":{"type":"git","url":"https:\/\/github.com\/cristianvuolo\/uploader.git","reference":"6834ca1220f9cbc1f0c0a570483dc164ea193788"},"dist":{"type":"zip","url":"https:\/\/api.github.com\/repos\/cristianvuolo\/uploader\/zipball\/6834ca1220f9cbc1f0c0a570483dc164ea193788","reference":"6834ca1220f9cbc1f0c0a570483dc164ea193788","shasum":""},"type":"library","time":"2017-02-07T20:10:08+00:00","autoload":{"psr-4":{"CristianVuolo\\Uploader\\":"src\/"}},"require":{"php":"\u003E=5.6.4","intervention\/image":"^2.3","laravel\/framework":"5.*"}},"1.0.0":{"name":"cristianvuolo\/uploader","description":"","keywords":[],"homepage":"","version":"1.0.0","version_normalized":"1.0.0.0","license":["MIT"],"authors":[],"source":{"type":"git","url":"https:\/\/github.com\/cristianvuolo\/uploader.git","reference":"37c03e90f2b52a5681453a7f7b0870c359eec649"},"dist":{"type":"zip","url":"https:\/\/api.github.com\/repos\/cristianvuolo\/uploader\/zipball\/37c03e90f2b52a5681453a7f7b0870c359eec649","reference":"37c03e90f2b52a5681453a7f7b0870c359eec649","shasum":""},"type":"library","time":"2017-02-07T20:01:41+00:00","autoload":{"psr-4":{"CristianVuolo\\Uploader\\":"src\/"}},"require":{"php":"\u003E=5.6.4","intervention\/image":"^2.3","laravel\/framework":"5.*"}}},"type":"library","repository":"https:\/\/github.com\/cristianvuolo\/uploader","github_stars":0,"github_watchers":1,"github_forks":0,"github_open_issues":1,"language":"PHP","dependents":0,"suggesters":0,"downloads":{"total":94,"monthly":5,"daily":0},"favers":0}}
\ No newline at end of file
diff --git a/test/datasource/__snapshots__/packagist.spec.js.snap b/test/datasource/__snapshots__/packagist.spec.js.snap
new file mode 100644
index 0000000000..66f0fe9e5f
--- /dev/null
+++ b/test/datasource/__snapshots__/packagist.spec.js.snap
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`datasource/packagist getDependency processes real data 1`] = `
+Object {
+  "homepage": "https://github.com/cristianvuolo/uploader",
+  "name": "cristianvuolo/uploader",
+  "repositoryUrl": "https://github.com/cristianvuolo/uploader",
+  "versions": Object {
+    "1.0.0": Object {
+      "gitHead": "1.0.0",
+      "time": "2017-02-07T20:01:41+00:00",
+    },
+    "1.0.1": Object {
+      "gitHead": "1.0.1",
+      "time": "2017-02-07T20:10:08+00:00",
+    },
+    "1.0.3": Object {
+      "gitHead": "1.0.3",
+      "time": "2017-07-11T16:52:54+00:00",
+    },
+    "1.0.4": Object {
+      "gitHead": "1.0.4",
+      "time": "2017-08-02T13:08:31+00:00",
+    },
+    "1.0.5": Object {
+      "gitHead": "1.0.5",
+      "time": "2017-08-02T13:23:33+00:00",
+    },
+    "1.0.6": Object {
+      "gitHead": "1.0.6",
+      "time": "2018-01-25T18:57:17+00:00",
+    },
+    "1.0.7": Object {
+      "gitHead": "1.0.7",
+      "time": "2018-02-19T12:22:34+00:00",
+    },
+    "1.0.8": Object {
+      "gitHead": "1.0.8",
+      "time": "2018-03-14T17:40:06+00:00",
+    },
+  },
+}
+`;
diff --git a/test/datasource/packagist.spec.js b/test/datasource/packagist.spec.js
new file mode 100644
index 0000000000..80f96ddd6b
--- /dev/null
+++ b/test/datasource/packagist.spec.js
@@ -0,0 +1,38 @@
+const fs = require('fs');
+const packagist = require('../../lib/datasource/packagist');
+const got = require('got');
+
+jest.mock('got');
+
+const res1 = fs.readFileSync('test/_fixtures/packagist/uploader.json');
+
+describe('datasource/packagist', () => {
+  describe('getDependency', () => {
+    it('returns null for empty result', async () => {
+      got.mockReturnValueOnce({});
+      expect(await packagist.getDependency('something')).toBeNull();
+    });
+    it('returns null for 404', async () => {
+      got.mockImplementationOnce(() =>
+        Promise.reject({
+          statusCode: 404,
+        })
+      );
+      expect(await packagist.getDependency('something')).toBeNull();
+    });
+    it('returns null for unknown error', async () => {
+      got.mockImplementationOnce(() => {
+        throw new Error();
+      });
+      expect(await packagist.getDependency('something')).toBeNull();
+    });
+    it('processes real data', async () => {
+      got.mockReturnValueOnce({
+        body: JSON.parse(res1),
+      });
+      expect(
+        await packagist.getDependency('cristianvuolo/uploader')
+      ).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/manager/composer/__snapshots__/extract.spec.js.snap b/test/manager/composer/__snapshots__/extract.spec.js.snap
new file mode 100644
index 0000000000..afa89b446d
--- /dev/null
+++ b/test/manager/composer/__snapshots__/extract.spec.js.snap
@@ -0,0 +1,253 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`lib/manager/pip_requirements/extract extractDependencies() extracts dependencies 1`] = `
+Object {
+  "deps": Array [
+    Object {
+      "currentValue": ">=5.3.2",
+      "depName": "php",
+      "depType": "require",
+      "purl": "pkg:packagist/php",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "symfony/assetic-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/symfony/assetic-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "symfony/monolog-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/symfony/monolog-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "symfony/swiftmailer-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/symfony/swiftmailer-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "2.1.*",
+      "depName": "symfony/symfony",
+      "depType": "require",
+      "purl": "pkg:packagist/symfony/symfony",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "2.2.2",
+      "depName": "doctrine/common",
+      "depType": "require",
+      "purl": "pkg:packagist/doctrine/common",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "doctrine/doctrine-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/doctrine/doctrine-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "doctrine/doctrine-fixtures-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/doctrine/doctrine-fixtures-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "2.2.x-dev",
+      "depName": "doctrine/orm",
+      "depType": "require",
+      "purl": "pkg:packagist/doctrine/orm",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "exercise/elastica-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/exercise/elastica-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "friendsofsymfony/rest-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/friendsofsymfony/rest-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "*",
+      "depName": "friendsofsymfony/user-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/friendsofsymfony/user-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "*",
+      "depName": "fzaninotto/faker",
+      "depType": "require",
+      "purl": "pkg:packagist/fzaninotto/faker",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "1.0.1",
+      "depName": "jms/di-extra-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/jms/di-extra-bundle",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "*",
+      "depName": "jms/payment-core-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/jms/payment-core-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "1.1.0",
+      "depName": "jms/security-extra-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/jms/security-extra-bundle",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "knplabs/knp-menu-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/knplabs/knp-menu-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "knplabs/knp-paginator-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/knplabs/knp-paginator-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "liip/imagine-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/liip/imagine-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "merk/dough-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/merk/dough-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "sensio/distribution-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/sensio/distribution-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "sensio/framework-extra-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/sensio/framework-extra-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "sensio/generator-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/sensio/generator-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "simplethings/entity-audit-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/simplethings/entity-audit-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "stof/doctrine-extensions-bundle",
+      "depType": "require",
+      "purl": "pkg:packagist/stof/doctrine-extensions-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "dev-master",
+      "depName": "twig/extensions",
+      "depType": "require",
+      "purl": "pkg:packagist/twig/extensions",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "2.3.*",
+      "depName": "behat/behat",
+      "depType": "require-dev",
+      "purl": "pkg:packagist/behat/behat",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "*",
+      "depName": "behat/behat-bundle",
+      "depType": "require-dev",
+      "purl": "pkg:packagist/behat/behat-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "*",
+      "depName": "behat/mink-bundle",
+      "depType": "require-dev",
+      "purl": "pkg:packagist/behat/mink-bundle",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "*",
+      "depName": "behat/sahi-client",
+      "depType": "require-dev",
+      "purl": "pkg:packagist/behat/sahi-client",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+    Object {
+      "currentValue": "*",
+      "depName": "behat/common-contexts",
+      "depType": "require-dev",
+      "purl": "pkg:packagist/behat/common-contexts",
+      "skipReason": "unsupported-constraint",
+      "verionScheme": "semver",
+    },
+  ],
+}
+`;
diff --git a/test/manager/composer/extract.spec.js b/test/manager/composer/extract.spec.js
new file mode 100644
index 0000000000..62869a2054
--- /dev/null
+++ b/test/manager/composer/extract.spec.js
@@ -0,0 +1,28 @@
+const fs = require('fs');
+const {
+  extractDependencies,
+} = require('../../../lib/manager/composer/extract');
+
+const requirements1 = fs.readFileSync(
+  'test/_fixtures/composer/composer1.json',
+  'utf8'
+);
+
+describe('lib/manager/pip_requirements/extract', () => {
+  describe('extractDependencies()', () => {
+    let config;
+    beforeEach(() => {
+      config = {};
+    });
+    it('returns null for invalid json', () => {
+      expect(extractDependencies('nothing here', config)).toBe(null);
+    });
+    it('returns null for empty deps', () => {
+      expect(extractDependencies('{}', config)).toBe(null);
+    });
+    it('extracts dependencies', () => {
+      const res = extractDependencies(requirements1, config);
+      expect(res).toMatchSnapshot();
+    });
+  });
+});
diff --git a/test/workers/repository/extract/__snapshots__/index.spec.js.snap b/test/workers/repository/extract/__snapshots__/index.spec.js.snap
index 9dcb229b6d..9cfcebb26f 100644
--- a/test/workers/repository/extract/__snapshots__/index.spec.js.snap
+++ b/test/workers/repository/extract/__snapshots__/index.spec.js.snap
@@ -11,6 +11,9 @@ Object {
   "circleci": Array [
     Object {},
   ],
+  "composer": Array [
+    Object {},
+  ],
   "docker": Array [
     Object {},
   ],
diff --git a/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap b/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap
index 3a2aed0f40..f5c2e4c8c4 100644
--- a/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap
+++ b/test/workers/repository/process/lookup/__snapshots__/index.spec.js.snap
@@ -80,6 +80,15 @@ Array [
 ]
 `;
 
+exports[`manager/npm/lookup .lookupUpdates() handles packagist 1`] = `
+Array [
+  Object {
+    "message": "Failed to look up dependency foo/bar",
+    "type": "warning",
+  },
+]
+`;
+
 exports[`manager/npm/lookup .lookupUpdates() handles prerelease jumps 1`] = `
 Array [
   Object {
diff --git a/test/workers/repository/process/lookup/index.spec.js b/test/workers/repository/process/lookup/index.spec.js
index b3454654ae..1b2dc4e01f 100644
--- a/test/workers/repository/process/lookup/index.spec.js
+++ b/test/workers/repository/process/lookup/index.spec.js
@@ -813,6 +813,16 @@ describe('manager/npm/lookup', () => {
         .reply(404);
       expect(await lookup.lookupUpdates(config)).toMatchSnapshot();
     });
+    it('handles packagist', async () => {
+      config.depName = 'foo/bar';
+      config.purl = 'pkg:packagist/foo/bar';
+      config.packageFile = 'composer.json';
+      config.currentValue = '1.0.0';
+      nock('https://packagist.org')
+        .get('/packages/foo/bar.json')
+        .reply(404);
+      expect(await lookup.lookupUpdates(config)).toMatchSnapshot();
+    });
     it('handles unknown purl', async () => {
       config.depName = 'foo';
       config.purl = 'pkg:typo/some/repo';
diff --git a/website/docs/configuration-options.md b/website/docs/configuration-options.md
index cba622f526..c20d8cdfce 100644
--- a/website/docs/configuration-options.md
+++ b/website/docs/configuration-options.md
@@ -124,6 +124,10 @@ This is used to alter `commitMessage` and `prTitle` without needing to copy/past
 
 This is used to alter `commitMessage` and `prTitle` without needing to copy/paste the whole string. The "topic" is usually refers to the dependency being updated, e.g. "dependency react".
 
+## composer
+
+Warning: composer support is in alpha stage so you probably only want to run this if you are helping get it feature-ready.
+
 ## copyLocalLibs
 
 Set to true if repository package.json files contain any local (file) dependencies + lock files. The `package.json` files from each will be copied to disk before lock file generation, even if they are within ignored directories.
@@ -436,6 +440,10 @@ Add to this object if you wish to define rules that apply only to patch updates.
 
 ## paths
 
+## php
+
+Warning: PHP Composer support is in alpha stage so you probably only want to run this if you are helping get it feature-ready.
+
 ## pin
 
 Add to this object if you wish to define rules that apply only to PRs that pin dependencies.
-- 
GitLab