Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.08% covered (success)
98.08%
51 / 52
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractExpireCommand
98.08% covered (success)
98.08%
51 / 52
80.00% covered (warning)
80.00%
4 / 5
7
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 configure
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
1
 getTimestampedMessage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
3
 getDateThreshold
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * Generic base class for expiration commands.
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2020.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  Console
25 * @author   Demian Katz <demian.katz@villanova.edu>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org/wiki/development Wiki
28 */
29
30namespace VuFindConsole\Command\Util;
31
32use DateTime;
33use Symfony\Component\Console\Command\Command;
34use Symfony\Component\Console\Input\InputArgument;
35use Symfony\Component\Console\Input\InputInterface;
36use Symfony\Component\Console\Input\InputOption;
37use Symfony\Component\Console\Output\OutputInterface;
38use VuFind\Db\Service\Feature\DeleteExpiredInterface;
39
40use function floatval;
41
42/**
43 * Generic base class for expiration commands.
44 *
45 * @category VuFind
46 * @package  Console
47 * @author   Demian Katz <demian.katz@villanova.edu>
48 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
49 * @link     https://vufind.org/wiki/development Wiki
50 */
51class AbstractExpireCommand extends Command
52{
53    /**
54     * Help description for the command.
55     *
56     * @var string
57     */
58    protected $commandDescription = 'Expiration tool';
59
60    /**
61     * Label to use for rows in help messages.
62     *
63     * @var string
64     */
65    protected $rowLabel = 'rows';
66
67    /**
68     * Minimum legal age (in days) of rows to delete.
69     *
70     * @var int
71     */
72    protected $minAge = 2;
73
74    /**
75     * Default age of rows (in days) to delete. $minAge is used if $defaultAge is
76     * null.
77     *
78     * @var int|null
79     */
80    protected $defaultAge = null;
81
82    /**
83     * Table on which to expire rows
84     *
85     * @var DeleteExpiredInterface
86     */
87    protected $table;
88
89    /**
90     * Constructor
91     *
92     * @param DeleteExpiredInterface $service Service on which to expire rows
93     * @param ?string                $name    The name of the command; passing null means it
94     * must be set in configure()
95     */
96    public function __construct(protected DeleteExpiredInterface $service, ?string $name = null)
97    {
98        parent::__construct($name);
99    }
100
101    /**
102     * Configure the command.
103     *
104     * @return void
105     */
106    protected function configure()
107    {
108        $this
109            ->setDescription($this->commandDescription)
110            ->setHelp("Expires old {$this->rowLabel} in the database.")
111            ->addOption(
112                'batch',
113                null,
114                InputOption::VALUE_REQUIRED,
115                'Number of records to delete in a single batch',
116                1000
117            )->addOption(
118                'sleep',
119                null,
120                InputOption::VALUE_REQUIRED,
121                'Milliseconds to sleep between batches',
122                100
123            )->addArgument(
124                'age',
125                InputArgument::OPTIONAL,
126                'Minimum age (in days, starting from '
127                    . number_format($this->minAge, 1, '.', '')
128                    . ") of {$this->rowLabel} to expire",
129                $this->defaultAge ?? $this->minAge
130            );
131    }
132
133    /**
134     * Add a time stamp to a message
135     *
136     * @param string $msg Message
137     *
138     * @return string
139     */
140    protected function getTimestampedMessage($msg)
141    {
142        return '[' . date('Y-m-d H:i:s') . '] ' . $msg;
143    }
144
145    /**
146     * Run the command.
147     *
148     * @param InputInterface  $input  Input object
149     * @param OutputInterface $output Output object
150     *
151     * @return int 0 for success
152     */
153    protected function execute(InputInterface $input, OutputInterface $output)
154    {
155        // Collect arguments/options:
156        $daysOld = floatval($input->getArgument('age'));
157        $batchSize = $input->getOption('batch');
158        $sleepTime = $input->getOption('sleep');
159
160        // Abort if we have an invalid expiration age.
161        if ($daysOld < $this->minAge) {
162            $output->writeln(
163                str_replace(
164                    '%%age%%',
165                    number_format($this->minAge, 1, '.', ''),
166                    'Expiration age must be at least %%age%% days.'
167                )
168            );
169            return 1;
170        }
171
172        // Calculate date threshold once to avoid creeping a few seconds in each loop iteration.
173        $dateLimit = $this->getDateThreshold($daysOld);
174
175        // Delete the expired rows--this cleans up any junk left in the database
176        // e.g. from old searches or sessions that were not caught by the session
177        // garbage collector. Records are deleted in batches until no more records to
178        // delete are found.
179        $total = 0;
180        do {
181            $count = $this->service->deleteExpired($dateLimit, $batchSize);
182            if ($count > 0) {
183                $output->writeln(
184                    $this->getTimestampedMessage("$count {$this->rowLabel} deleted.")
185                );
186                $total += $count;
187                // Be nice to others and wait between batches
188                usleep($sleepTime * 1000);
189            }
190        } while ($count > 0);
191
192        $output->writeln(
193            $this->getTimestampedMessage("Total $total {$this->rowLabel} deleted.")
194        );
195        return 0;
196    }
197
198    /**
199     * Convert days to a date threshold
200     *
201     * @param float $daysOld Days before now
202     *
203     * @return DateTime
204     */
205    protected function getDateThreshold(float $daysOld): DateTime
206    {
207        return new DateTime("now - $daysOld days");
208    }
209}