symplely/coroutine

View on GitHub
Coroutine/Misc/ContextTrait.php

Summary

Maintainability
A
1 hr
Test Coverage
<?php

declare(strict_types=1);

namespace Async\Misc;

use Async\TaskInterface;

/**
 * A **context manager** can control a block of code using the `async_with()` and `ending()` functions.
 * Basically a `try {} catch {} finally {}` construct, with any `resource` or `object` used be automatically **closed**.
 *
 * @see https://realpython.com/python-with-statement/
 * @see https://book.pythontips.com/en/latest/context_managers.html
 * @see https://docs.python.org/3/glossary.html#term-context-manager
 * @see https://www.python.org/dev/peps/pep-0343/
 */
trait ContextTrait
{
  /**
   * @var resource|object
   */
  protected $context;

  /**
   * @var object
   */
  protected $instance;

  /**
   * @var TaskInterface
   */
  protected $withTask = null;

  /**
   * @var boolean
   */
  protected $withSet = false;

  /**
   * @var \Throwable
   */
  protected $error = null;
  protected $isObject = false;
  protected $done = false;

  /**
   * `__enter()` execution status.
   *
   * @var boolean
   */
  protected $enter = false;

  /**
   * `__exit()` execution status.
   *
   * @var boolean
   */
  protected $exit = false;


  /**
   * Return the `Task` instance a context manager _instance_ is **attached** to by `async_with()` or `with()`.
   * DO NOT OVERWRITE THIS METHOD.
   *
   * @return TaskInterface|null
   */
  public function withTask(): ?TaskInterface
  {
    return $this->withTask;
  }

  /**
   * Set a `async_with()` or `with()` **Task** _instance_ to **active** and assign a context manager _instance_ to it.
   * DO NOT OVERWRITE THIS METHOD.
   * - This function needs to be prefixed with `yield`
   *
   * @return void
   * @throws \Error If not call from inside an `async()` created function, or method.
   * - Use/call `yield method_task();` first inside a regular function/method.
   */
  public function withSet()
  {
    $task = \coroutine()->getTask(yield \current_task());
    if (!$task->isAsync() && !$task->isAsyncMethod()) {
      \panic(new \Error("Can only use `async_with()` or `with()` inside an `async()` created function, or method!" . \EOL . "Use/call `yield method_task();` first inside a regular function/method." . \EOL));
    }

    $this->withTask = $task->setWith($this);
    $this->withSet = true;
  }

  /**
   * Clear/remove a `async_with()` or `with()` **Task** _instance_ from context manager, and set to **not active**.
   * DO NOT OVERWRITE THIS METHOD.
   *
   * @return void
   */
  public function clearWith(): void
  {
    if ($this->withTask instanceof TaskInterface)
      $this->withTask->setWith();

    $this->withTask = null;
    $this->withSet = false;
  }

  /**
   * Has `async_with()` or `with()` _assigned_ a context manager to a `Task` that _is_ **active**.
   * DO NOT OVERWRITE THIS METHOD.
   *
   * @return boolean
   */
  public function isWith(): bool
  {
    return $this->withSet;
  }

  /**
   * Returns the status of `__enter()` execution.
   * DO NOT OVERWRITE THIS METHOD.
   *
   * @return boolean
   */
  public function entered(): bool
  {
    return $this->enter;
  }

  /**
   * Context managers use this method to create the desired context for the execution of the contained code.
   * This method WILL BE called by `__invoke()`.
   *
   * - When **overwriting** this method, `$this->enter` _property_ MUST BE set to `true`, otherwise a `Panic` exception will be **throw**.
   *
   *```php
   *    // This check SHOULD BE added to SOME METHOD or ALL `YIELDING` METHODS to insure `with` is set.
   *    if (!$this->isWith()) {
   *      yield $this->withSet();
   *    }
   *```
   *
   * @return mixed
   */
  public function __enter()
  {
    $this->enter = true;
  }

  /**
   * Returns the status of `__exit()` execution.
   * DO NOT OVERWRITE THIS METHOD.
   *
   * @return boolean
   */
  public function exited(): bool
  {
    return $this->exit;
  }

  /**
   * Context managers use this method to clean up after execution of the contained code.
   * This method WILL BE called by `__invoke()`.
   *
   * - When **overwriting** this method, `$this->exit` _property_ MUST BE set to `true`, otherwise a `Panic` exception will be **throw**.
   * - When **overwriting** this method, `$this->error` _property_ MUST BE set to `$type`, to propagate _errors_, if **caller** not handling.
   * - When **overwriting** this method, `parent::__destruct()` _method_ SHOULD BE called to **insure** proper code clean up is preformed.
   *
   * @param \Throwable|null $type
   * @return mixed
   */
  public function __exit(\Throwable $type = null)
  {
    if (!empty($type)) {
      $this->error = $type;
    }

    $this->exit = true;
    $this->__destruct();
  }

  /**
   * Preform local context manager code block clean up, pre releasing memory and any additional resources.
   *
   * - When **overwriting** this method, `parent::__destruct()` _method_ SHOULD BE called
   * to **insure** proper code clean up is preformed.
   *
   * @return void
   */
  public function __destruct()
  {
    if (!$this->exit) {
      $this->__exit();
    }

    if (!$this->done)
      $this->close();
  }

  /**
   * @param resource $context
   * @param object|null $object with a `close()` method defined.
   */
  public function __construct($context, ...$object)
  {
    $this->context = $context;
    if (!empty($object))
      $this->instance = \array_shift($object);

    if (\is_object($this->instance)) {
      if (\method_exists($this->instance, 'close'))
        $this->isObject = true;
      else
        \panic('Not valid object instance, missing `close()` method!');
    }
  }

  /**
   * This method forms the heart of a context manager execution flow. Called by `async_with()`, `with()`, and `ending()` functions.
   *
   * This method then executes either `__enter()` or `__exit()` depending on state, only once.
   * - When **overwriting** this method, insure the replacement action executes the methods to at least set the proper state as in changeable section below:
   *
   *```php
   * public function __invoke()
   *  {
   * // Do not change!
   * if (!$this->withSet) {
   *   yield $this->withSet();
   * }
   *
   * // Is changeable...
   *    if (!$this->enter) {
   *      return $this->__enter();
   *    } elseif (!$this->exit) {
   *      return $this->__exit();
   *    }
   *  }
   *```
   *
   * @return mixed
   */
  public function __invoke()
  {
    // Do not change!
    if (!$this->withSet) {
      yield $this->withSet();
    }

    // Is changeable!
    if (!$this->enter) {
      return $this->__enter();
    } elseif (!$this->exit) {
      return $this->__exit();
    }
  }

  /**
   * Will execute the methods in a `DI` container or `object` supplied at `__construct($context, $object)`.
   * DO NOT OVERWRITE THIS METHOD.
   * - Future version of this _context_ manager will have preset `asynchronous` **magic method** actions for various resource types.
   *
   * @param string $function
   * @param mixed $args
   *
   * @return mixed
   * @throws \Error If `function` _method_ does not exist
   */
  public function __call($function, $args)
  {
    if (!$this->withSet) {
      yield $this->withSet();
    }

    if ($this->isObject && \method_exists($this->instance, $function)) {
      try {
        $this->done = null;
        return $this->instance->$function(...$args);
      } catch (\Throwable $e) {
        $this->done = false;
        $this->error = $e;
      } finally {
        if ($this->done !== null)
          $this->close();
      }
    } else {
      $this->error = new \Error("$function does not exist");
      $this->close();
    }
  }

  /**
   * Return any PHP builtin or resource like object that was supplied for _context_ at `__construct($context, $object)`.
   *
   * @return resource
   */
  public function getResource()
  {
    return $this->context;
  }

  /**
   * Preforms the proper context managers code block clean up.
   * **Closing** resources and **execute** any _instance/object_ `close()` method that was
   * supplied at `__construct($context, $object)` from a **DI** Container.
   *
   * - When **overwriting** this method, `parent::__destruct()` _method_ SHOULD BE called
   * to **insure** proper code clean up is preformed.
   *
   * @return void
   */
  public function close(): void
  {
    if ($this->withSet)
      $this->clearWith();

    if (\is_resource($this->context) && \get_resource_type($this->context) == 'stream')
      \fclose($this->context);

    if ($this->isObject)
      $this->instance->close();

    unset($this->context);
    unset($this->instance);

    $this->context = null;
    $this->instance = null;
    $this->done = true;

    if ($this->error) {
      $error = $this->error;
      $this->error = null;
      if ($error instanceof \Throwable)
        throw $error;
    }
  }
}