Strategy to override a class in a library installed with Composer
Asked Answered
H

5

46

I am using Codeigniter and Composer. One of the requirements is PHPExcel. Now I need to change a function in one of the classes. What should be the best strategy to do it? Should I change the code in the vendor folder? If so, how to maintain the change across all the instances? If not how do I override that particular class. Though I mention PHPExcel I would like a generic solution.

I am not sure if this is the right forum for this question. If not i will remove this. Please let me know if any more details are needed.

Thank You.

Hop answered 23/1, 2015 at 6:58 Comment(1)
Oh yeah, PHPExcel, same reason to be here.Earthbound
H
68

In composer.json, under ["autoload"]["psr-4"], add an entry with namespace as the key and path as the value:

{
     "autoload": {

         "psr-4": {

             "BuggyVendor\\Namespace\\": "myfixes/BuggyVendor/Namespace"
         }
     }
}

Copy files you want to override under that path (keeping sub-namespace directory structure) and edit them there. They will be picked in preference to the library package's original "classpath". It would seem that namespace->path mappings added to composer.json in this manner are considered before those added by required packages. Note: I just tried it and it worked, though I don't know if it is an intended feature or what possible gotchas are.

EDIT: found a gotcha. Sometimes when you subsequently require another package with composer require vendor/package, you will "lose" the override. If this happens, you must issue composer dump-autoload manually. This will restore the correct autoload order honoring your override.

Histoplasmosis answered 4/1, 2016 at 21:21 Comment(7)
You are positively awesome. I've been trying to find a way to override Doctrine's horrible EntityGenerator, and you've given me exactly what I needed. You are a HERO.Chaperon
Using this approach you'll probably get something like: ``` Warning: Ambiguous class resolution, "Doctrine\Common\Collections\ArrayCollection" was found in both "vendor/doctrine/collections/lib/Doctrine/Common/Collections/ArrayCollection.php" and "src\Doctrine\Common\Collections\ArrayCollection.php", the first will be used. ``` to overcome this issue add original class to exclusion: ``` "exclude-from-classmap": ["vendor/doctrine/collections/lib/Doctrine/Common/Collections/ArrayCollection.php"] ```, and then autoloader will resolve your override class.Picker
@OlegAndreyev This should be an answer on it's own. This was very helpful and a solution exactly to my problem. Thank you very much!Aesop
@OlegAndreyev Simply remove the former class from the autoloader to prevent the Ambiguous class resolution warning from happening: "autoload": {"exclude-from-classmap": ["vendor/path-to-file-to-ignore.php"]}.Sikes
It is important that you include the full namespace of your override class in the autoload entry, since composer autoload will look first in the most specific namespace directories, and the longer namespace in the override will put it up at the top of that list. It's not so important where the physical directory is located, but keeping it as a PSR-4 diectory path will make it easier to follow when you come back to it.Genera
is there a way to only override a specific function in case the original class is too big and we only want to change one tiny function? Is it possible to load the original case with another name and inherit it in the new class?Schleswigholstein
@Schleswigholstein You could change the original class to a trait, rename it, and remove all extends and implements. Then create a new class with the old name, with extends and implements from the original class, then use the trait, then override the method you want. It is not possible to completely avoid duplicating and modifying the original class file, but at least in this solution the modification is purely mechanical and independent of the customization you want to do, and the actual customization resides in a separate file.Anatollo
H
24

Adding these last 2 lines to the autoload section of my composer.json is what worked for me when I wanted to override just one file within the vendors directory:

"autoload": {        
    "classmap": [
        "database"
    ],
    "psr-4": {
        "App\\": "app/"
    },
    "exclude-from-classmap": ["vendor/somepackagehere/blah/Something.php"],
    "files": ["app/Overrides/Something.php"]
},

Remember that the namespace within app/Overrides/Something.php needs to match whatever the original was in vendor/somepackagehere/blah/Something.php.

Remember to run composer dump-autoload after editing the composer.json.

Docs: https://getcomposer.org/doc/04-schema.md#files

Honeydew answered 11/11, 2019 at 17:36 Comment(2)
Works as expected!Weinstein
Remember to run composer dump-autoload +1Nonstop
B
15

There is one more option. In case you need to rewrite the only class you can use files in composer.json like this

 "autoload": {
     "files": ["path/to/rewritten/Class.php"]
  }

So if you want to rewrite class Some\Namespace\MyClass put it like this

#path/to/rewritten/Class.php

namespace Some\Namespace;

class MyClass {
  #do whatever you want here
}

Upon each request composer will load that file into memory, so when it comes to use Some\Namespace\MyClass - implementation from path/to/rewritten/Class.php will be used.

Boger answered 9/8, 2017 at 10:48 Comment(1)
This will lead to an Ambiguous class resolution warning, when using composer dumpautoload -a or -o and will use the original file instead of the overridden one. Szczepan's solution also outputs this warning, like Oleg pointed out above, but it will resolve to using the custom class instead of the original one.Sikes
I
0

You can also simply copy the file over and overwrite the original file with your own. Assuming creating a directory for example "vendor-overrides" where you place your fixed file, just add this to you composer:

"scripts": {
    "post-install-cmd": [
      "@php -r \"copy('vendor-overrides/path/to/your/fixed/file.php', 'vendor/path/to/your/broken/file.php');\""
    ],
    "post-update-cmd": [
"@php -r \"copy('vendor-overrides/path/to/your/fixed/file.php', 'vendor/path/to/your/broken/file.php');\""
    ]
  }
Immolation answered 6/8, 2023 at 21:41 Comment(0)
S
-17

Changing an existing class is against OOP and SOLID principles (Open to extension/Closed for modification principle specificaly). So the solution here is not to change the code directly, but to extend the code to add your functionnality.

In an ideal world you should never change a piece of code that you don't own. In fact, with composer you can't because your change will be overrided when updating dependencies.

A solution in your case is to create a class at the application level, and extend the class you want to change (which is at the library level) to override with your code. Please look at extending a class in PHP if you don't know how.

Then typically, you load your class instead of their class, this way, you add your functionnality on top of their functionnality, and in case of an update, nothing break (in case of a non breaking update).

Stifling answered 23/1, 2015 at 7:44 Comment(4)
This answer is useless 99% of the time. A typical scenario where replacing a class is unavoidable is when the framework is not designed to be told which class to use. It uses a class named Library\LibraryClass, full stop, and simply cannot be configured to use App\MyClassDerivedFromLibraryClass instead. The asker asked VERY specifically how to correctly hook into Composer's autoload so that it would look for Library\LibraryClass first in my directory, then in the library's directory. Treat us to a sermon on OOP principles in addition to, not instead of answering the question.Anatollo
If your framework isn't designed to changes classes freely, then maybe you should consider using another FW? I agree that, as stated, my answer only fit in a perfect world. It does also perfectly fit the OP question because the OP can change which class is instantiated. Feel free to ask another, more specific, question if this one doesn't fit for you.Stifling
I don't see anything in the OP question that hints at the OP being able to change which class is instantiated. On the contrary, the fact that they specifically mention Composer hints at their inability to do so. I don't know of a way to specify with Composer which class is to be instantiated. That's DI. See my answer below, to follow shortly.Anatollo
If you have an issue with a function in a class that is then used all over the place, you can't just "create a class at the application level". You'd need to replicate or extend every single class that used that class, without adding anything to any of them aside from changing which class was imported. That is a completely asinine way of doing things, and OP was trying to avoid such a terrible scenario. Take Doctrine's EntityGenerator for example. It's garbage, but hard-coded all over the place. You can't just subclass that sort of disastrous hierarchy where the weak part is the foundation.Chaperon

© 2022 - 2024 — McMap. All rights reserved.