43

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.

Blair McMillan
  • 5,299
  • 2
  • 25
  • 45
SNAG
  • 2,011
  • 2
  • 23
  • 43

5 Answers5

65

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.

Szczepan Hołyszewski
  • 2,707
  • 2
  • 25
  • 39
  • 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. – Gerald Thibault Feb 18 '16 at 02:40
  • 20
    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. – Oleg Andreyev Jun 09 '16 at 14:35
  • 1
    @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! – Tony Bogdanov Sep 19 '17 at 12:03
  • 3
    @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"]}`. – flu Dec 05 '17 at 15:50
  • 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. – Jason Aug 26 '21 at 11:39
  • 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? – Haider Dec 29 '21 at 20:24
  • @Haider 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. – Szczepan Hołyszewski Dec 29 '21 at 21:42
20

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

Ryan
  • 22,332
  • 31
  • 176
  • 357
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.

Thor Samsberg
  • 2,219
  • 5
  • 22
  • 30
  • 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. – flu Dec 05 '17 at 13:31
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');\""
    ]
  }
pdolinaj
  • 1,087
  • 13
  • 21
-16

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).

Atrakeur
  • 4,126
  • 3
  • 17
  • 22
  • 19
    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. – Szczepan Hołyszewski Jan 03 '16 at 08:08
  • 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. – Atrakeur Jan 03 '16 at 10:56
  • 1
    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. – Szczepan Hołyszewski Jan 04 '16 at 21:15
  • 4
    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. – Gerald Thibault Feb 18 '16 at 02:28