DEV Community

Aleksander Wons
Aleksander Wons

Posted on

Developing and debugging a library alongside your main project

This is a copy of an old post on my private blog from July 2022.

Intro

Disclaimer: The following article assumes the following setup: Linux or Mac host, Docker, and docker-compose. I haven't tried it on a Windows host, It may work, but I have no idea how the links (NTFS links) will behave when mounted into a container.

It's not uncommon to have a project and one or more libraries that are used together with it. When developing in such an environment, we strive to keep the library separated from the project with clear and easy-to-understand interfaces. We do not only to keep things clean but also to ensure that the library can be developed and adapted without the constant need to test it against the main project.

But sometimes, this is precisely what we want to do. To work on the library and test it within the boundaries of our main project. While we developed, we may need to debug things so that xdebug will come in handy.

Usually, the directory layout on our development machine will look something like this:

├── main_project/
│   ├── src/
│   ├── composer.json
├── some_library/
│   ├── src/
│   ├── composer.json
Enter fullscreen mode Exit fullscreen mode

The "main_project" has a dependency on the library "some_library." The library will be published somewhere in a production environment, like private Packagist, Bitbucket, GitLab, etc.). To load it, we use composer:

composer require owner/some_library
Enter fullscreen mode Exit fullscreen mode

That will get the library from the remote repository and symlink into:

main_project/vendor/owner/some_library
Enter fullscreen mode Exit fullscreen mode

While this is perfect for our production or staging environment, it becomes more impractical for the local development.

When we develop a library locally, we often want to be able to test it within our main project. We may want to know if we are going in the right direction in terms of interfaces or to do some functional/integration tests for the main project that uses our library and add code, refactor, or fix issues on the way.

There is a feature in composer that we can use to our advantage, and instead of downloading the library package from the remote repo, we can reference it locally. To do this, we have to tell the composer where to look for the library. The following code needs to be added to the composer.json of the main project.

{
    "minimum-stability": "dev",
    "repositories": [
        {
            "type": "path",
            "url": "/full/path/to/some_library"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

This configuration tells the composer that some extra repositories must be considered when resolving dependencies. This specific repository type called "path" means the composer will look for that dependency under the provided path. We used a full path, but a relative one would also work. A small caveat: if we want to load the latest development state, we need to allow for minimum stability "dev." Otherwise, we can only request a tagged version of our library. We need to make sure to revert that change before committing to whatever value we had before.

If we now run:

composer require owner/some_library
Enter fullscreen mode Exit fullscreen mode

composer will symlink:

/full/path/to/some_library -> main_project/vendor/owner/some_library
Enter fullscreen mode Exit fullscreen mode

You may now ask: How did the composer know to load the local copy and not the remote one? This is simple. Priority. Repositories defined as local have priority over remote ones. As soon as the composer is able to find a matching path locally, it will stop looking elsewhere.

Debugging inside a docker container

If our development environment was a local one, we wouldn't have to do anything more. We do have a symbolic link now, but since both paths are within the same FS scope, the PHP will be able to follow it.

When working with Docker, we have to manually share the local path. Usually, our "docker-compose.yml" will look something like this:

version: '3'
services:
  php:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/application
Enter fullscreen mode Exit fullscreen mode

The critical part, "volumes," tells us we will only map our local directory into "/application" inside the container. But because the library is symlinked, it's technically outside of our project's path. And therefore, PHP will not be able to see it.

That's an easy thing to solve. We have to map the linked path into the container. Both the local and the remote paths must be the same (because this is what the container sees - the linked path):

volumes:
  - .:/application
  - /full/path/to/some_library:/full/path/to/some_library
Enter fullscreen mode Exit fullscreen mode

This already allows us to run the code properly inside the container. We still have to configure one thing if we want to be able to debug inside the library. That is because, by default, PhpStorm (and any IDE/editor on that matter) will only map the main path. So if our project is under "/full/path/to/my_project," and we mount it inside the container under "/application," we will only have this one mapping:

/full/path/to/my_project -> /application
Enter fullscreen mode Exit fullscreen mode

We don't need to add another mapping. The one for the library. We have to remember what we configured in "docker-compose.yml." Both the local and the remote paths are the same.

/full/path/to/some_library -> /full/path/to/some_library
Enter fullscreen mode Exit fullscreen mode

Summary

As we can see, it's not that difficult to make it work. We need to jump over a few hoops, but in the end, we can play with both the main project and the library as if they were one. The only drawback is - we have to remember to revert the settings in composer before we commit the project code. But this is the price to pay.

We could try to cut some corners and symlink the library but do not change anything in the composer. Why wouldn't I recommend this? It would only work if all of the dependencies of our libraries were already installed before symlinking. After that, we cannot update the library and do anything composer related because it will just wipe our symlink.

Top comments (0)