1<?php
2
3/* Copyright (c) 2019 Richard Klees <richard.klees@concepts-and-training.de> Extended GPL, see docs/LICENSE */
4
5use ILIAS\Setup\Environment;
6use ILIAS\Setup\CallableObjective;
7use ILIAS\Setup\NullObjective;
8use ILIAS\Setup\Objective;
9
10/**
11 * This base-class simplifies the creation of (consecutive) database updates.
12 *
13 * Implement update steps on one or more tables by creating methods that follow
14 * this schema:
15 *
16 * public function step_1(\ilDBInterface $db) { ... }
17 *
18 * The class will figure out which of them haven't been performed yet and need
19 * to be executed.
20 *
21 * If the class takes care of only one table or a set of related tables it will
22 * be easier to maintain.
23 *
24 * If for some reason you rely on other objectives, e.g. steps from other db-update
25 * classes, implement `getAdditionalPreconditionsForStep`.
26 */
27abstract class ilDatabaseUpdateSteps implements Objective
28{
29    const STEP_METHOD_PREFIX = "step_";
30
31    /**
32     * @var	string[]|null
33     */
34    protected $steps = null;
35
36    /**
37     * @var	Objective
38     */
39    protected $base;
40
41    /**
42     * @param \ilObjective $base for the update steps, i.e. the objective that should
43     *                           have been reached before the steps of this class can
44     *                           even begin. Most probably this should be
45     *                           \ilDatabasePopulatedObjective.
46     */
47    public function __construct(
48        Objective $base
49    ) {
50        $this->base = $base;
51    }
52
53    /**
54     * Get preconditions for steps.
55     *
56     * The previous step will automatically be a precondition of every step but
57     * will not be returned from this method.
58     *
59     * @return Objective[]
60     */
61    public function getAdditionalPreconditionsForStep(int $num) : array
62    {
63        return [];
64    }
65
66    /**
67     * The hash for the objective is calculated over the classname and the steps
68     * that are contained.
69     */
70    final public function getHash() : string
71    {
72        return hash(
73            "sha256",
74            get_class($this)
75        );
76    }
77
78    final public function getLabel() : string
79    {
80        return "Database update steps in " . get_class($this);
81    }
82
83    /**
84     * @inheritdocs
85     */
86    final public function isNotable() : bool
87    {
88        return true;
89    }
90
91    /**
92     * @inheritdocs
93     */
94    final public function getPreconditions(Environment $environment) : array
95    {
96        $log = $environment->getResource(\ilDatabaseUpdateStepExecutionLog::class);
97
98        if ($log) {
99            $finished = $log->getLastFinishedStep(get_class($this));
100        } else {
101            $finished = 0;
102        }
103
104        return [$this->getStep($this->getLatestStepNum(), $finished)];
105    }
106
107    /**
108     * @inheritdocs
109     */
110    final public function achieve(Environment $environment) : Environment
111    {
112        return $environment;
113    }
114
115    /**
116     * Get a database update step. Optionally tell which step is known to have
117     * been finished to exclude it from the preconditions of the newer steps.
118     *
119     * @throws \LogicException if step is unknown
120     */
121    final public function getStep(int $num, int $finished = 0) : ilDatabaseUpdateStep
122    {
123        $cur = $this->base;
124        foreach ($this->getSteps() as $s) {
125            if ($s <= $finished) {
126                continue;
127            } elseif ($s <= $num) {
128                $cur = new ilDatabaseUpdateStep($this, $s, $cur, ...$this->getAdditionalPreconditionsForStep($s));
129            } else {
130                break;
131            }
132        }
133
134        return $cur;
135    }
136
137    /**
138     * Get the number of latest database step in this class.
139     */
140    final public function getLatestStepNum() : int
141    {
142        $this->getSteps();
143        return end($this->steps);
144    }
145
146    /**
147     * Get the numbers of the steps in this class.
148     *
149     * @return int[]
150     */
151    final protected function getSteps() : array
152    {
153        if (!is_null($this->steps)) {
154            return $this->steps;
155        }
156
157        $this->steps = [];
158
159        foreach (get_class_methods(static::class) as $method) {
160            if (stripos($method, self::STEP_METHOD_PREFIX) !== 0) {
161                continue;
162            }
163
164            $number = substr($method, strlen(self::STEP_METHOD_PREFIX));
165
166            if (!preg_match("/^[1-9]\d*$/", $number)) {
167                throw new \LogicException("Method $method seems to be a step but has an odd looking number");
168            }
169
170            $this->steps[(int) $number] = (int) $number;
171        }
172
173        asort($this->steps);
174
175        return $this->steps;
176    }
177}
178