-1

I have two subclasses and I want to call a method in one subclass from another. Is the only solution to start chaining my classes in order of dependence? Sorry for the noob PHP question. Basically it's a common scenario to have multiple "base" classes such as an email class in which other subclasses will need access to. So do you just starting chaining them?

// BaseClass.php

require_once("./services/checkout.php");
require_once("./services/email.php");
class Base {
    // ...
}
// checkout.php
    
class checkout extends Base {
    public function onCheckout() {
        // Call send email
        $this->sendEmail($email); // <- How to do this?
    }
}
// email.php

class email extends Base {
    public function sendEmail($email) {
        // Send email
    }
}
Will B.
  • 17,883
  • 4
  • 67
  • 69
AlxVallejo
  • 3,066
  • 6
  • 50
  • 74
  • hi, it is not possible to do what you are looking for, you should probably chain inheritance. Instead, i recommend you to explore the possibility to inject instance of `Email` on `Checkout` as service. For that you will probably need dependency injection container. – Yanis-git Mar 11 '22 at 18:15
  • In your example, what is the `Base` class intended to do and why do you believe it is needed? Is it acting as a container for configurations or some form of controller of sorts? It appears that you're attempting to implement your own form of Event Listener functionality or a Service container. – Will B. Mar 11 '22 at 22:27

3 Answers3

2

I think it's common to have BaseAction or BaseController but not something as generic as BaseClass. It feels more intuitive to have something like:

class Checkout
{
    public function onCheckout(Mailer $mailer)
    {
        $mailer->sendEmail($email);
    }
}

class Mailer
{
    public function sendEmail($email)
    {
        // Send email
    }
}

This is still very rough. You most likely want an interface, injected into checkout constructor, perhaps automagically with some dependency injection library that implements autowiring:

interface MessengerInterface
{
    public function send(string $text): bool;
}
class Checkout
{
    
    public function __construct(
       private MessengerInterface $messenger
    ) {
    }

    public function onCheckout()
    {
        $this->messenger->send('Your order blah blah');
    }
}
class Mailer implements MessengerInterface
{
    public function send(string $body): bool
    {
        // Send email
    }
}
Will B.
  • 17,883
  • 4
  • 67
  • 69
Álvaro González
  • 142,137
  • 41
  • 261
  • 360
2

The key phrase here is "composition vs inheritance".

Inheritance represents an "is-a" relationship. You might say that an HTMLEMail is an Email - it's the same kind of thing, but with extra or different behaviour. But it doesn't make sense to say Checkout is an Emailer - they're two different things, neither is a type of the other.

Composition represents a "has-a" relationship. A Checkout might have an Emailer, which it uses to send e-mails. So they are different classes that you "compose" together. That basically means that rather than $this->sendEmail(), you would write $emailer->sendEmail(), where $emailer is another object that's come from somewhere.

The cleanest way to implement "composition" is via "dependency injection", which is mostly a fancy way of saying "passing objects to other objects".

That might be passing it into the individual method:

class Checkout extends Base {
    public function doCheckout($emailer) {
        // Call send email
        $emailer->sendEmail('email@example.com', 'Hello');
    }
}

More flexibly, since you might want to use in multiple methods, passing it into the constructor and storing it on a property:

class Checkout extends Base {
    private Emailer $emailer;

    public function __construct(Emailer $emailer) {
        $this->emailer = $emailer;
    }

    public function doCheckout() {
        // Call send email
        $this->emailer->sendEmail('email@example.com', 'Hello');
    }
}

This kind of setting of properties in the constructor is so common that PHP 8 has a special short-hand for it called "Constructor Property Promotion":

class Checkout extends Base {
    // note the "private" keyword next to the argument
    public function __construct(private Emailer $emailer) {
    }

    public function doCheckout() {
        // Call send email
        $this->emailer->sendEmail('email@example.com', 'Hello');
    }
}

More flexibly still, you can depend on an interface, which means "anything that has the right behaviour can be passed in here, I'll decide on the exact implementation later".

The idea is that you can write the Checkout class without knowing what the inside of the Emailer class is going to look like. You just need to know that sendEmail requires certain arguments, and returns certain things when it's done. Then you can have one implementation that uses a username and password to talk to an SMTP server, one implementation that uses an API key to use a web service, and a third that just logs to a local file for test purposes. The Checkout class doesn't need to know how to create each one, or when to use them, it just waits for one to be provided.

Somewhere else, you have some co-ordinating code that actually knows how to create all these objects. The fancy way to do this in a larger project is with a library like PHP-DI that uses configuration and "auto-wiring" to work out what's needed when, but a simple version just looks like this:

$config = parse_config_file('config.ini');
if ( $config['mode'] === 'debug' ) {
    $emailer = new DebugEmailer($config['logfiles']['email_debug']);
}
else {
    $emailer = new SMTPEmailer($config['smtp']['username'], $config['smtp']['password']);
}
$checkout = new Checkout($emailer);
$checkout->doCheckout();
IMSoP
  • 89,526
  • 13
  • 117
  • 169
  • Thanks, but what i don't get is what if you need to instantiate the emailer class with some vars, e.g. an api key var that comes from the base class? You can't do that in any of your examples. – AlxVallejo Mar 11 '22 at 21:04
  • @AlxVallejo if you're not using a DI (dependency injection) framework with autowiring like [PHP-DI](https://php-di.org/), you'll need to instantiate the emailer class and pass it as an argument manually. An alternative to DI is the [`Factory pattern`](https://en.wikipedia.org/wiki/Factory_%28object-oriented_programming%29) to standardize on the instantiation of the object(s) and simplify replication. However the Factory pattern is considered a lot less favorable and harder to read/maintain overtime as opposed to using DI which wraps everything in a pretty bow. – Will B. Mar 11 '22 at 21:41
  • @AlxVallejo You need to step back further: there is no "base class" for the vars to come from. There might be a *co-ordinating* class somewhere that says "to make an `Emailer`, I need to get this config string; once I've made the `Emailer`, I can pass it to the `Checkout`..." But `Checkout` doesn't have an "is-a" relationship with that co-ordinator, it shouldn't _care_ what kind of parameters the `Emailer` needs. – IMSoP Mar 11 '22 at 21:49
  • @AlxVallejo That's also the point with requiring based on an interface not a class name: you might have one implementation of the `EmailerInterface` that needs an API key, another that needs a username and password, and a third that just logs to a local file for test purposes. You don't want to have logic in the `Checkout` class that knows how to create each one, and decides which to use - it's not the *responsibility* of that class. – IMSoP Mar 11 '22 at 21:53
  • @AlxVallejo Answer expanded based on above comments. – IMSoP Mar 11 '22 at 22:02
  • @IMSoP So basically I ended up just instantiating my email class in my parent class and made it inheritable to any subclass. This isn't exactly getting into the weeds of DI or factories or interfaces, but I prefer minimizing overhead. – AlxVallejo Mar 14 '22 at 17:27
  • @AlxVallejo There's no "overhead" to minimise; code structure is about writing a program that you (and others) can understand and work on. I strongly recommend you get rid of the idea of a universal base class, it will cause you no end of problems. – IMSoP Mar 14 '22 at 17:30
  • @IMSoP At the end of the day, you need to instantiate one parent class that basically bootstraps your service or whatever module you're working on. If i remove my base class, there's nothing to instantiate and none of my code will work. – AlxVallejo Mar 14 '22 at 17:55
  • 1
    @AlxVallejo No matter what classes and functions you define, they won't do anything if you don't call them. If you have one class, you write "new" once, if you have two you write it twice, there's nothing magic about it. You could put everything into one long function and it would be easy to *call*, but missing the point of functions; similarly, making everything the responsibility of one base class is missing the point of classes and inheritance. – IMSoP Mar 14 '22 at 18:02
0

Please read Traits in Php. It seem that would be the right thing for your goal:

PHP-Traits (php.org)

PHP-Traits (W3Schools - easier to understand)

I would suggest to reconsider your inheritance, whether it makes sense that E-Mail class and Checkout-Class inherit from the same base class. They should do total different and independent things by their own. If you want to send an e-mail from the checkout-class then try to implement an e-mail class and inject an instance of it to the checkout object.

Selim Acar
  • 378
  • 2
  • 9
  • Well what should i use ... Traits or DI? – AlxVallejo Mar 11 '22 at 18:35
  • @AlxVallejo I am not a trait expert and I do not know in which cases it is the best solution. On the other hand, from your pseudo code it is not clear what you really want to inherit and how dependent your classes are etc. But in general DI is a good thing. It brings better developing and testing possibilities. Imagine you define an interface IEmail and your Email class implements this interface. Then you can define FakeEmail, which also implements the same interface. You can use FakeEmail as e-mail mock so long as you are working on checkout. – Selim Acar Mar 11 '22 at 18:42
  • @AlxVallejo And when you are done with checkout, you can start writing the real E-mail class. At the end you need just to inject the instance of the Email class instead of FakeEmail class. – Selim Acar Mar 11 '22 at 18:45
  • @AlxVallejo A Trait is essentially a copy/paste of the `use`'d script, allowing calling methods under the context as `$this` but with limitations. DI will provide you with a service oriented architecture to define your services and inject them as dependencies into objects that require them. Traits have their purposes as does DI. In this instance you would be looking more at DI, since you would not want to bloat all of your objects that need to send email with a copy of the mailer as it is used as a service. – Will B. Mar 11 '22 at 21:02