Great question! This is a classic case where polymorphism, interfaces, and the strategy/factory patterns can really help. Here are a couple of clean solutions to keep your code maintainable, reusable, and testable.
1. Use an Interface + Specific Implementations
First, create an interface which each context-specific class must implement:
interface VariableLoader
{
public function loadVariables();
}
Then, implement this interface in each class (e.g., for Table and Package):
class TableVariableLoader implements VariableLoader
{
public function loadVariables()
{
// Table-specific logic
}
}
class PackageVariableLoader implements VariableLoader
{
public function loadVariables()
{
// Package-specific logic
}
}
Now, your CalculationService would depend on the VariableLoader interface:
class CalculationService
{
protected VariableLoader $loader;
public function __construct(VariableLoader $loader)
{
$this->loader = $loader;
}
public function loadVariables()
{
return $this->loader->loadVariables();
}
}
And to use it:
$service = new CalculationService(new TableVariableLoader());
// or
$service = new CalculationService(new PackageVariableLoader());
2. Use a Factory to Decide Which Implementation to Use
If you often need to resolve the right loader class based on the model passed in, a simple factory might help:
class LoaderFactory
{
public static function make($context)
{
if ($context instanceof Table) {
return new TableVariableLoader();
}
if ($context instanceof Package) {
return new PackageVariableLoader();
}
throw new \Exception('No VariableLoader found for this context');
}
}
Usage:
$loader = LoaderFactory::make($table);
$service = new CalculationService($loader);
3. (Alternative) Use Method Injection
If you don’t want to instantiate the service every time, you could allow passing the "context" to your CalculationService::loadVariables($context) method, then handle branching inside the service. But generally, composition (injecting dependencies) as above is the cleaner way.
Summary:
The interface/strategy approach (option 1) gives you the most flexibility, testability, and maintainability. Combine it with a Factory if you need to resolve the right implementation automatically. Avoid duplicating code in every Livewire component—prefer composition over inheritance or copy-pasting methods.
Let me know if you’d like to see the implementation in more detail for any of these options!