102 lines
9.6 KiB
Markdown
102 lines
9.6 KiB
Markdown
|
[https://downing.tech/posts/overriding-vendor-classes](https://downing.tech/posts/overriding-vendor-classes)
|
||
|
|
||
|
Ever found yourself wanting to make a small tweak to a PHP file in a Composer dependency? Here's how to do it without forking the entire package.
|
||
|
|
||
|
![[gDnEeyW8nabpCJ7rjBOay6jC29oLcpcf39aO0n0P.jpg]]
|
||
|
|
||
|
If you take a look at my blog, you'll see that I make frequent and heavy use of code blocks. Being a developer, this is an extremely important part of my content. It has to be right. On my old blog, I used to use CSS to give code blocks a nice background and mono font, but it just wasn't engaging enough.
|
||
|
|
||
|
For my [2021 blog update](https://downing.tech/posts/2021-year-in-review), I decided to use [Torchlight](https://torchlight.dev/) to add some visual interest and a clearer reading experience to my posts. If you haven't heard of Torchlight, go check it out; it's an outstanding product by [Aaron Francis](https://twitter.com/aarondfrancis) and team.
|
||
|
|
||
|
When I went to install Torchlight, I encountered a potential showstopper. I'm using [Wink](http://github.com/themsaid/wink) for writing my posts, which ships with its own `WinkPost` Eloquent model. That model has a dynamic attribute, `content`, which looks like this.
|
||
|
|
||
|
```
|
||
|
public function getContentAttribute()if (! $this->markdown) {return $this->body;$converter = new GithubFlavoredMarkdownConverter(['allow_unsafe_links' => false,return new HtmlString($converter->convertToHtml($this->body));
|
||
|
```
|
||
|
|
||
|
Now, if you're not aware, `GithubFlavoredMarkdownConverter` is part of the [League Commonmark](https://github.com/thephpleague/commonmark) package, and performs the work of converting our markdown to HTML. For Torchlight to be able to add syntax highlighting to our markdown code blocks, we need to add an extension to the convertor. The issue is, we don't have access to this code. The `WinkPost` lives inside the `vendor` folder and isn't made publicly available. _Gulp_.
|
||
|
|
||
|
## Potential solutions
|
||
|
|
||
|
So, what's to be done? We could of course fork the entirety of the Wink codebase. Doesn't that seem overkill for one line of code though? It does to me.
|
||
|
|
||
|
We could instead access the content via another class, like a `ContentManager`. You would pass it a `$post->body`, and it would create its own `GithubFlavoredMarkdownConverter`, add the `TorchlightExtension`, and return the results. However, this would cost us the elegance of simply calling `$post->content` and automatically being provided with renderable HTML. You can also imagine that on a larger project, this could easily be missed and implemented differently in two separate locations.
|
||
|
|
||
|
Another option, which is no doubt the best solution in the long run, is to create a PR with your fix to the original repository. I will cover writing PRs for 3rd party libraries in a future post. By creating a PR, everybody can benefit from your solution, including your future self. However, this can take time because OSS maintainers already have a **lot** on their plate. They need to find time in their busy schedules to review, comment, test, discuss and ship your PR.
|
||
|
|
||
|
Is there a solution that's fast, requires little maintenance overhead, and can easily be reverted in the future, say once your PR is merged? Why yes. Yes there is.
|
||
|
|
||
|
## Overriding vendor files with Composer
|
||
|
|
||
|
In case you didn't know, all of your project dependencies are managed using a service called [Composer](https://getcomposer.org/). In all honesty, if you didn't know that, you should probably come back to this post once you're a little more familiar with the ecosystem, because what I'm about to show you is a very [sharp knife](https://m.signalvnoise.com/provide-sharp-knives/) that, used incorrectly, can cause more harm than good.
|
||
|
|
||
|
Composer is configured by a json file at the root of your project, aptly named `composer.json`. Let's take a look at that file.
|
||
|
|
||
|
```
|
||
|
"name": "lukeraymonddowning/blog","type": "project","description": "My personal blog.","keywords": ["framework", "laravel"],"license": "MIT","require": {"php": "^8.0","abraham/twitteroauth": "^3.2","andreiio/blade-remix-icon": "^1.0","fruitcake/laravel-cors": "^2.0","guzzlehttp/guzzle": "^7.0.1","laravel/framework": "^8.65","laravel/sanctum": "^2.11","laravel/tinker": "^2.5","nedwors/navigator": "^0.2.0","spatie/laravel-feed": "^4.0","spatie/laravel-health": "^1.5","thecodingmachine/safe": "^1.3","themsaid/wink": "^1.2","torchlight/torchlight-commonmark": "^0.5.2""autoload": {"psr-4": {"App\\": "app/","Database\\Factories\\": "database/factories/","Database\\Seeders\\": "database/seeders/"
|
||
|
```
|
||
|
|
||
|
I've focused in on one particular section of the `composer.json` file: the `autoload` node. When I first started writing PHP, autoloading was something of a mystery to me. When I needed a dependency, I would just include it at the top of the PHP script. Fun times.
|
||
|
|
||
|
```
|
||
|
include_once __DIR__ . '/MyClass.php';
|
||
|
```
|
||
|
|
||
|
Autoloading removes all of that manual pain by using [namespaces](https://www.php.net/manual/en/language.namespaces.rationale.php) to automatically load in the correct files. Laravel sort of hides this from us, but if you take a look at `public/index.php`, you'll see that it includes the autoloader for us.
|
||
|
|
||
|
```
|
||
|
use Illuminate\Contracts\Http\Kernel;define('LARAVEL_START', microtime(true));| we will load this file so that any pre-rendered content can be shownif (file_exists(__DIR__.'/../storage/framework/maintenance.php')) {require __DIR__.'/../storage/framework/maintenance.php';| this application. We just need to utilize it! We'll simply require it| into the script here so we don't need to manually load our classes.require __DIR__.'/../vendor/autoload.php';| Once we have the application, we can handle the incoming request using| to this client's browser, allowing them to enjoy our application.$app = require_once __DIR__.'/../bootstrap/app.php';$kernel = $app->make(Kernel::class);$response = tap($kernel->handle($request = Request::capture()$kernel->terminate($request, $response);
|
||
|
```
|
||
|
|
||
|
Okay, what's this got to do with anything? Well, Composer actually allows us to "hack in" to this autoloading process. We can tell Composer to avoid automatically loading a file at a specific namespace. In this case, the namespace to avoid will be `Wink\WinkPost`. All we have to do is add a new `exclude-from-classmap` entry to the `autoload` node in our `composer.json` file.
|
||
|
|
||
|
```
|
||
|
"autoload": {"exclude-from-classmap": ["Wink\\WinkPost""psr-4": {"App\\": "app/","Database\\Factories\\": "database/factories/","Database\\Seeders\\": "database/seeders/"
|
||
|
```
|
||
|
|
||
|
At this point, your project is broken. If you've referenced `Wink\WinkPost` anywhere in your project, it will no longer be able to find it. So, we need to replace that file with our own version. Let's copy the full contents of `vendor/themsaid/wink/src/WinkPost.php` and paste it in a new file: `.overrides/WinkPost.php`. The actual folder name doesn't matter, I've just called it `.overrides` for clarity. _You must ensure that the namespace is identical to the original._
|
||
|
|
||
|
Now, we'll add another entry to the `autoload` node: `files`.
|
||
|
|
||
|
```
|
||
|
"autoload": {"exclude-from-classmap": ["Wink\\WinkPost""files": [".overrides/WinkPost.php""psr-4": {"App\\": "app/","Database\\Factories\\": "database/factories/","Database\\Seeders\\": "database/seeders/"
|
||
|
```
|
||
|
|
||
|
Notice that we include our custom `WinkPost.php` file in that array. Think of the `files` node as a dumb array of files that Composer will load regardless of namespacing standards. Because we are using the `Wink\\WinkPost` namespace in our file, any reference to it in our codebase will call our custom file instead. Our application will be none the wiser, but we now have full access to the `WinkPost` class. How cool is that?
|
||
|
|
||
|
One more thing. You probably need to dump the autoloader after these changes to make sure that Composer has registered your new file.
|
||
|
|
||
|
```
|
||
|
composer dump-autoload
|
||
|
```
|
||
|
|
||
|
## Editing our new file
|
||
|
|
||
|
With that done, we can make any changes we'd like to `.overrides/WinkPost.php`, and they'll be reflected in our application. Let's add support for Torchlight.
|
||
|
|
||
|
```
|
||
|
public function getContentAttribute()if (! $this->markdown) {return $this->body;$converter = new GithubFlavoredMarkdownConverter(['allow_unsafe_links' => false,$converter->getEnvironment()->addExtension(new TorchlightExtension());return new HtmlString($converter->convertToHtml($this->body));
|
||
|
```
|
||
|
|
||
|
Done! That wasn't so bad now was it?
|
||
|
|
||
|
## Caveats and gotchas
|
||
|
|
||
|
No solution is without its cons. It's important to remember that if the original project updates the file you've overridden, you'll need to manually copy over those changes. Because of that, use this very sparingly. It may also be a good idea to make it very clear what you have actually altered in the file so that you can easily reference that when making updates.
|
||
|
|
||
|
```
|
||
|
public function getContentAttribute()if (! $this->markdown) {return $this->body;$converter = new GithubFlavoredMarkdownConverter(['allow_unsafe_links' => false,$converter->getEnvironment()->addExtension(new TorchlightExtension());return new HtmlString($converter->convertToHtml($this->body));
|
||
|
```
|
||
|
|
||
|
## Conclusion
|
||
|
|
||
|
By making full use of Composer, we're able to override classes that would otherwise be uneditable to suit our needs. I've made use of this in my blog, in the [pest parallel plugin](https://github.com/pestphp/pest-plugin-parallel/tree/master/build), and in a number of other projects.
|
||
|
|
||
|
If you find yourself having to override more than a couple of files from a dependency, it's likely an indication that you should fork that project instead and maintain your own branch of it.
|
||
|
|
||
|
Still, the speed and simplicity of class overrides in cases like this one can really come in handy, and is an invaluable tool to have in your developer kit for when the need arises.
|
||
|
|
||
|
Thanks for reading!
|
||
|
|
||
|
Kind Regards, Luke
|