diff --git a/lib/manager/composer/__snapshots__/artifacts.spec.ts.snap b/lib/manager/composer/__snapshots__/artifacts.spec.ts.snap
index cd337bf740d73a4f7a4f10b0a73578764f1b11e2..67c8cb2c772a7aa6e1673f992c1f29d25cf5c8af 100644
--- a/lib/manager/composer/__snapshots__/artifacts.spec.ts.snap
+++ b/lib/manager/composer/__snapshots__/artifacts.spec.ts.snap
@@ -178,6 +178,30 @@ Array [
 ]
 `;
 
+exports[`.updateArtifacts() supports vendor directory update 1`] = `
+Array [
+  Object {
+    "cmd": "composer update --with-dependencies --ignore-platform-reqs --no-ansi --no-interaction --no-scripts --no-autoloader",
+    "options": Object {
+      "cwd": "/tmp/github/some/repo",
+      "encoding": "utf-8",
+      "env": Object {
+        "COMPOSER_CACHE_DIR": "/tmp/renovate/cache/others/composer",
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "maxBuffer": 10485760,
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
 exports[`.updateArtifacts() uses hostRules to set COMPOSER_AUTH 1`] = `
 Array [
   Object {
diff --git a/lib/manager/composer/artifacts.spec.ts b/lib/manager/composer/artifacts.spec.ts
index 5a4b7bce65219df9ef6cfe52b2af6b8ecc5d58f9..9882a19afb47af8bef27d3450c7d0df14ecdea8f 100644
--- a/lib/manager/composer/artifacts.spec.ts
+++ b/lib/manager/composer/artifacts.spec.ts
@@ -128,6 +128,37 @@ describe('.updateArtifacts()', () => {
     ).not.toBeNull();
     expect(execSnapshots).toMatchSnapshot();
   });
+  it('supports vendor directory update', async () => {
+    const foo = join('vendor/foo/Foo.php');
+    const bar = join('vendor/bar/Bar.php');
+    const baz = join('vendor/baz/Baz.php');
+
+    fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
+    const execSnapshots = mockExecAll(exec);
+    git.getRepoStatus.mockResolvedValueOnce({
+      modified: ['composer.lock', foo],
+      not_added: [bar],
+      deleted: [baz],
+    } as StatusResult);
+    fs.readLocalFile.mockResolvedValueOnce('New composer.lock' as any);
+    fs.readLocalFile.mockResolvedValueOnce('Foo' as any);
+    fs.readLocalFile.mockResolvedValueOnce('Bar' as any);
+    fs.getSiblingFileName.mockReturnValueOnce('vendor' as any);
+    const res = await composer.updateArtifacts({
+      packageFileName: 'composer.json',
+      updatedDeps: [],
+      newPackageFileContent: '{}',
+      config,
+    });
+    expect(res).not.toBeNull();
+    expect(res?.map(({ file }) => file)).toEqual([
+      { contents: 'New composer.lock', name: 'composer.lock' },
+      { contents: 'Foo', name: foo },
+      { contents: 'Bar', name: bar },
+      { contents: baz, name: '|delete|' },
+    ]);
+    expect(execSnapshots).toMatchSnapshot();
+  });
   it('performs lockFileMaintenance', async () => {
     fs.readLocalFile.mockResolvedValueOnce('Current composer.lock' as any);
     const execSnapshots = mockExecAll(exec);
diff --git a/lib/manager/composer/artifacts.ts b/lib/manager/composer/artifacts.ts
index 8c153c4fecac378543b80a0f99562a99374ce917..9f5d09901ee13b3756ca52058ec435fb2da9105b 100644
--- a/lib/manager/composer/artifacts.ts
+++ b/lib/manager/composer/artifacts.ts
@@ -110,7 +110,9 @@ export async function updateArtifacts({
     logger.debug('No composer.lock found');
     return null;
   }
-  await ensureLocalDir(getSiblingFileName(packageFileName, 'vendor'));
+
+  const vendorDir = getSiblingFileName(packageFileName, 'vendor');
+  await ensureLocalDir(vendorDir);
   try {
     await writeLocalFile(packageFileName, newPackageFileContent);
     if (config.isLockFileMaintenance) {
@@ -150,7 +152,7 @@ export async function updateArtifacts({
       return null;
     }
     logger.debug('Returning updated composer.lock');
-    return [
+    const res: UpdateArtifactsResult[] = [
       {
         file: {
           name: lockFileName,
@@ -158,6 +160,27 @@ export async function updateArtifacts({
         },
       },
     ];
+
+    for (const f of status.modified.concat(status.not_added)) {
+      if (f.startsWith(vendorDir)) {
+        res.push({
+          file: {
+            name: f,
+            contents: await readLocalFile(f),
+          },
+        });
+      }
+    }
+    for (const f of status.deleted || []) {
+      res.push({
+        file: {
+          name: '|delete|',
+          contents: f,
+        },
+      });
+    }
+
+    return res;
   } catch (err) {
     if (
       err.message?.includes(