Search code examples
phpcomposer-php

Composer not autoloading classes when local package used from another


I'm creating a wrapper package for the Google API that I only want to use locally, and I'm looking to include it in other local projects with composer via namespaces rather than local repository.

Trying to use it in a test project is resulting in PHP Fatal error: Uncaught Error: Class "Google_Client" not found. But it works fine in the original package.

I had this working, and then I don't know what changed and now it's not working.

I made a clean test to demonstrate the problem:

├── companyname
│   └── googletools
│       ├── bin
│       ├── composer.json
│       ├── composer.lock
│       ├── config
│       ├── src
│       └── vendor
└── testproject
    ├── bin
    │   └── test.php
    ├── composer.json
    ├── composer.lock
    ├── src
    └── vendor
        ├── autoload.php
        └── composer

googletools/composer.json is:

{
    "name": "companyname/googletools",
    "version": "1.0.0",
    "require": {
        "google/apiclient": "^2.17"
    },
    "autoload": {
        "psr-4": {
            "CompanyName\\GoogleTools\\": "src/"
        }
    }
}

testproject/composer.json is

{
    "name": "companyname/testproject",
    "version": "1.0.0",
    "require": {
    },
    "autoload": {
        "psr-4": {
            "Testproject\\": "src/",
            "CompanyName\\GoogleTools\\": "../companyname/googletools/src"
        }
    }
}

googletools/bin/test.php is:

<?php

require __DIR__.'/../vendor/autoload.php';

$sheetId = 'mysheetid;

$sheet = new CompanyName\GoogleTools\Sheet($sheetId);

$sheetName = 'mysheetname';

$range = $sheetName;

$response = $sheet->spreadsheets_values->get($sheetId, $range);
$values = $response->getValues();

print_r($values);

The above test.php works fine, outputs as expected.

testproject/bin/test.php is:

<?php

require __DIR__.'/../vendor/autoload.php';

use CompanyName\GoogleTools\Sheet;

$sheetId = 'mysheetid;

$sheet = new CompanyName\GoogleTools\Sheet($sheetId);

$sheetName = 'mysheetname';

$range = $sheetName;

$response = $sheet->spreadsheets_values->get($sheetId, $range);
$values = $response->getValues();

print_r($values);

Output is:

PHP Fatal error: Uncaught Error: Class "Google_Client" not found in /Path/to/companyname/googletools/src/Sheet.php:22

Sheet.php starts:

<?php

namespace CompanyName\GoogleTools;

use Google_Client;
use Google_Service_Sheets;

Why would this work for the first test.php but not the second?


Solution

  • The first thing to get clear is the relationship between namespaces, autoloading and Composer.

    • A namespace is basically a way of ensuring class names in one library won't conflict with names in another library, without having to continuously write long names like CompanyName_GoogleTools_Sheet.
    • Autoloading is basically a way of telling PHP what include or require statements are needed to load the definition of a class, so that you don't have to load every class you might need in advance.
    • Composer is primarily a way to specify a list of packages to install, resolve all their dependencies, and download them. Since it knows where it installed the files, it can set up an autoloader function for them. That's what you're activating with the line require __DIR__.'/../vendor/autoload.php';

    So, let's see what's happening in your example:

    • use Google_Client; is to do with namespaces; it's just a compiler directive to say "in this file, if I mention Google_Client, I mean \Google_Client". It doesn't trigger any autoloading.
    • Later, presumably, you run new Google_Client or similar. This does trigger autoloading.
    • When you run companyname/googletools/bin/test.php, the autoloader you have loaded is the one in companyname/googletools/vendor/autoload.php, which is based on companyname/googletools/composer.json. It knows that it installed google/api-client into the directory companyname/googletools/vendor/google/api-client. It also knows that that package includes the class Google_Client, so loads it for you.
    • When you run testproject/bin/test.php, the autoloader you have loaded is the one in testproject/vendor/autoload.php, based on testproject/composer.json. That currently knows that classes beginning Testproject\ are in src/, and classes beginning CompanyName\GoogleTools are in companyname/googletools/, but it hasn't installed anything (other than Composer's internal machinery) in testproject/vendor. It doesn't know about any file that contains the class Google_Client.

    The simplest solution is to list google/api-client in the requires section of testproject/composer.json. That way, Composer will install a copy into testproject/vendor/google/api-client, and be able to autoload it for you.

    For Composer to look into the dependencies of the "googletools" library, it needs to be treating it as a separate package which "testproject" depends on, rather than just a directory which (as far as it knows) is inside the same project.

    For maximum flexibility, you can set up your "googletools" library as a private Composer package, and Composer will download and install it in any project where you need it.

    As a simpler option with your current layout, you can configure a "path" repository in testproject/composer.json.

    With either of these options (private package, or path repository), you will list companyname/googletools in the requires list of testproject/composer.json. Then, Composer will look into the dependencies of the "googletools" package, and install an appropriate version, just like it does when you require a public package.