Hey,
This overview seems quite reasonable to me until this point:
on the other hand it [using ie dependency injection] would mean a lot of
gadgets break every time we change things, and some possibly do even if we don't.
I am unsure how you are reaching that conclusion.
Dependency Injection, and more generally speaking Inversion of Control, is an often useful approach in creating cohesive code with low coupling. However in itself it does not magically make your architecture and code quality good. You can still create low quality code and a bad architecture when using Dependency Injection (granted, it's easier to do so when not using DI).
If gadgets break whenever you change something, you are suffering from whats called fragility. The main cause for this is coupling that should not be there. On an architecture level this means there is something wrong with your boundaries. That is the first thing I'd be scrutinizing in your situation.
Some sort of hook system (with try-catch blocks, strict
validation etc) would be much more stable
I'm not sure either approach is inherently more stable. Stability depends a lot on how you are implementing it. The main thing to hold in mind here for either approach is to be careful with what you expose. Narrow well defined interfaces are typically the way you want to go. When exposing to much, one ends up in a situation where flexibility and stability are at odds - ie either you cannot make a certain change in your code, or you need to break the public interface.
Decision: go with the closed model; reach out for potential plugin writers
and collect requirements; do not guess, only add plugin functionality where it is actually requested by someone.
What I like about this is that you are not randomly adding a bunch of hooks. Each hook decreases your flexibility a tiny bit and each hook is additional maintenance cost. If you introduce them on an as-needed basis and make sure your hook has a nice interface and well defined goal, then you are well on your way to nicely implementing this.
This approach is however not disjoint with Inversion of Control and good architecture, which you apparently agree with:
most of it is just good architecture like
services or dependency injection which would make sense even if we did not want plugins
So I do hope you will also be pursuing that road.
It is not clear to me from your email that you are holding the difference between libraries and applications into account, so I'll briefly explain.
Either a component is reusable or not, having a component with reusable and non-reusable code in it, and trying to reuse that reusable code from it is bad practice. Reusable code typically ends up in libraries. These libraries have no state. Non-reusable components, such as applications and extensions, typically do have their own state. If you want some code to be truly reusable, going with the library approach, as the Symfony Components are doing, is almost certainly the right approach. And indeed, this to is merely generally good practice one should probably follow even when not having the explicit goal to make something reusable.
The problem with having this supposedly reusable functionality in one component with state that cannot be used as library is that it forces as users to work together. It is like a global variable or a static class. When modifying the thing (ie via a hook), one needs to be careful to not do this in a way that breaks the other users. When at some point two different users have constraints for the state that conflict, you have a problem. At that point you are faced with the choice between painful refactoring or creating an even bigger mess.
Cheers
-- Jeroen De Dauw - http://www.bn2vs.com Software craftsmanship advocate Evil software architect at Wikimedia Germany ~=[,,_,,]:3