<?php

// Интрефейс чего-то, что можно включить и выключить
interface SwitchInterface {
    function turnOn();   // Включить
    function turnOff();  // Выключить
    function isOn();     // Возвращает true, если включено
    function isOff();    // Возвращает true, если выключено
    function state();    // Возвращает текущее состояние
}

// Какая-то стратегия, которая что-то делает
interface StrategyInterface {
    function apply();  // Что-то делаем
}

// Какой-то прибор
abstract class DeviceAbstract implements SwitchInterface {
    const STATE_ON = 'on';    // Состояние "Включено"
    const STATE_OFF = 'off';  // Состояние "Выключено"

    private $state = self::STATE_OFF;  // Текущее состояние объекта (по умолчанию "Выключено")

    // Включить
    function turnOn() {
        $this->state = self::STATE_ON;
    }

    // Выключить
    function turnOff() {
        $this->state = self::STATE_OFF;
    }

    // Проверить, "включен" ли объект
    function isOn() {
        return $this->state === self::STATE_ON;
    }

    // Проверить, "выключен" ли объект
    function isOff() {
        return $this->state === self::STATE_OFF;
    }

    // Вернуть текущее состояние объекта
    function state() {
        return $this->state;
    }
}

// Раширение для реализации событий
trait Events {
    private $eventSubscribers = [];  // Подписчики

    // Добавляем подписчика
    function addSubscriber(callable $subscriber) {
        $this->eventSubscribers[] = $subscriber;
    }

    // Уведомляем подписчиков о событии
    function notify($data) {
        foreach($this->eventSubscribers as $subscriber) {
            // Вызываем каждого подписчика и передаём ему себя и данные
            $subscriber($this, $data);
        }
    }
}

// Вспомогательная функция для генерации имени объекта
function get_object_name($object) {
    if (is_object($object)) {
        return get_class($object) . '(' . spl_object_id($object) . ')';
    } else {
        return gettype($object);
    }
}

// Стратегия включения лампы
class TurnOnStrategy implements StrategyInterface {
    use Events;

    function apply($subject = null) {
        // Если есть, что включать
        if ($subject instanceof SwitchInterface) {
            // Если оно выключено
            if ($subject->isOff()) {
                // Отсылаем уведомление
                $this->notify('Устройство ' . get_object_name($subject) . ' выключено, включаем');

                // Включаем
                $subject->turnOn();
            } else {
                // Если оно уже включено, ничего не делаем
                $this->notify('Устройство ' . get_object_name($subject) . ' уже включено, ничего не делаем');
            }
        }
    }
}

// Лампа
class Lamp extends DeviceAbstract {
    use Events;

    // Перегружаем метод "Включить"
    function turnOn() {
        $this->notify('Включаем лампу');
        parent::turnOn();
    }

    // Перегружаем метод "Выключить"
    function turnOff() {
        $this->notify('Выключаем лампу');
        parent::turnOff();
    }
}

// Некто
class Person {
    use Events;

    // Применить стратегию к переключателю
    function applyStrategyToSwitch(StrategyInterface $strategy, SwitchInterface $device) {
        // Отсылаем уведомление
        $this->notify(sprintf('Применяем стратегию %s к %s',
            get_object_name($strategy),
			get_object_name($device)
		));

		$strategy->apply($device);
	}
}

class BrokenLamp extends Lamp {
    private $attempts = 0;

    // Перегружаем метод "Включить"
    function turnOn() {
        if (++$this->attempts < 3) {
            $this->notify('Что-то заело, не могу включить');
        } else {
            parent::turnOn();
        }
    }

    // Перегружаем метод "Выключить"
    function turnOff() {
        // Сбрасываем попытки включения
        $this->attempts = 0;
        parent::turnOff();
    }
}

// Тройное включение
class TripleTurnOnStrategy extends TurnOnStrategy {
    function apply($subject = null) {
        parent::apply($subject);
        parent::apply($subject);
        parent::apply($subject);
    }
}

// Создаём объекты
$lamp = new BrokenLamp();
$turnOn = new TripleTurnOnStrategy();
$person = new Person();

// Создаём обработчик событий, который будет выводить собыяти на экран
$displayLogger = function ($object, $text) {
    echo get_object_name($object), ": $text\n";
};

// Подписываемся на события разных объектов
$lamp->addSubscriber($displayLogger);
$person->addSubscriber($displayLogger);
$turnOn->addSubscriber($displayLogger);

// Несколько раз включаем лампу
$person->applyStrategyToSwitch($turnOn, $lamp);
$person->applyStrategyToSwitch($turnOn, $lamp);
