Bug 36432: Remove circular dependency from Koha::Object

Koha::Object depends on Koha::DateUtils, which depends on C4::Context,
which depends on Koha::Config::SysPref, which depends on... Koha::Object

Apart from the circular dependency, the dependency on C4::Context alone
is problematic as it loads a bunch of modules that are not needed at all
in Koha::Object (YAML::XS and ZOOM for instance).
As Koha::Object is used as a base for a lot of modules, we should take
care to only load the minimum required.

This patch moves some date parsing code to specific modules:
- Koha::DateTime::Format::RFC3339
- Koha::DateTime::Format::SQL

and it uses them in Koha::Object and Koha::DateUtils where it is
possible.

Test plan:
1. Do not apply the patch yet and run the following command:
   `perl -cw Koha/Object.pm`
   It should print several warnings about redefined subroutines, meaning
   there is a circular dependency.
2. Apply the patch
3. Run `perl -cw Koha/Object.pm`. It should only say:
   "Koha/Object.pm syntax OK"
4. Run the following command:
    prove \
        t/DateUtils.t \
        t/Koha/DateTime/Format/RFC3339.t \
        t/db_dependent/Koha/Object.t

Signed-off-by: Victor Grousset/tuxayo <victor@tuxayo.net>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de>
(cherry picked from commit 8d5299639e)
Signed-off-by: Fridolin Somers <fridolin.somers@biblibre.com>
This commit is contained in:
Julian Maurice 2024-03-26 16:19:41 +01:00 committed by Fridolin Somers
parent f6ebb311e5
commit b0c862533d
7 changed files with 248 additions and 43 deletions

View File

@ -0,0 +1,94 @@
package Koha::DateTime::Format::RFC3339;
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
=head1 NAME
Koha::DateTime::Format::RFC3339 - Parse and format RFC3339 dates
=head1 SYNOPSIS
$datetime = Koha::DateTime::Format::RFC3339->parse_datetime($rfc3339_datetime_string);
$rfc3339_datetime_string = Koha::DateTime::Format::RFC3339->format_datetime($datetime);
=head1 API
=head2 Class methods
=head3 parse_datetime
Parse an RFC3339 datetime string and returns a corresponding L<DateTime> object
$datetime = Koha::DateTime::Format::RFC3339->parse_datetime($rfc3339_datetime_string);
=cut
use Modern::Perl;
use DateTime::Format::Builder (
parsers => {
parse_datetime => [
{
params => [qw( year month day hour minute second time_zone )],
regex =>
qr/^(\d{4})-(\d{2})-(\d{2})[Tt\s](\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?([Zz]|(?:[\+|\-](?:[01][0-9]|2[0-3]):[0-5][0-9]))$/,
postprocess => \&_postprocess_datetime,
},
],
}
);
=head3 format_datetime
Format a L<DateTime> object into an RFC3339 datetime string
$rfc3339_datetime_string = Koha::DateTime::Format::RFC3339->format_datetime($datetime);
=cut
sub format_datetime {
my ( $class, $dt ) = @_;
my $date = $dt->strftime('%FT%T%z');
substr( $date, -2, 0, ':' ); # timezone "HHmm" => "HH:mm"
return $date;
}
=head2 Internal methods
=head3 _postprocess_datetime
Called by C<parse_datetime> after parsing the datetime string.
It allows to change C<DateTime::new> parameters just before C<parse_datetime>
calls it.
=cut
sub _postprocess_datetime {
my %args = @_;
my $parsed = $args{parsed};
# system allows the 0th of the month
$parsed->{day} = '01' if $parsed->{day} eq '00';
$parsed->{time_zone} = 'UTC' if $parsed->{time_zone} =~ /^[Zz]$/;
return 1;
}
1;

View File

@ -0,0 +1,66 @@
package Koha::DateTime::Format::SQL;
# This file is part of Koha.
#
# Koha is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# Koha is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
=head1 NAME
Koha::DateTime::Format::SQL - Parse SQL dates
=head1 SYNOPSIS
$datetime = Koha::DateTime::Format::SQL->parse_datetime($sql_datetime_string);
=cut
use Modern::Perl;
use DateTime::Format::MySQL;
use Koha::Config;
our $timezone;
=head1 API
=head2 Class methods
=head3 parse_datetime
Parse an SQL datetime string and returns a corresponding L<DateTime> object
$datetime = Koha::DateTime::Format::SQL->parse_datetime($rfc3339_datetime_string);
DateTime's time zone is automatically set to the configured timezone (or
'local' if none is configured), unless the year is 9999 in which case the
timezone is 'floating'.
=cut
sub parse_datetime {
my ( $class, $date ) = @_;
my $dt = DateTime::Format::MySQL->parse_datetime($date);
# No TZ for dates 'infinite' => see bug 13242
if ( $dt->year < 9999 ) {
$timezone //= Koha::Config->get_instance->timezone;
$dt->set_time_zone($timezone);
}
return $dt;
}
1;

View File

@ -20,6 +20,7 @@ use Modern::Perl;
use DateTime;
use C4::Context;
use Koha::Exceptions;
use Koha::DateTime::Format::RFC3339;
use vars qw(@ISA @EXPORT_OK);
BEGIN {
@ -72,6 +73,10 @@ sub dt_from_string {
return $date_string->clone();
}
if ($date_format eq 'rfc3339') {
return Koha::DateTime::Format::RFC3339->parse_datetime($date_string);
}
my $regex;
# The fallback format is sql/iso
@ -113,28 +118,6 @@ sub dt_from_string {
(?<year>\d{4})
|xms;
}
elsif ( $date_format eq 'rfc3339' ) {
$regex = qr/
(?<year>\d{4})
-
(?<month>\d{2})
-
(?<day>\d{2})
([Tt\s])
(?<hour>\d{2})
:
(?<minute>\d{2})
:
(?<second>\d{2})
(\.\d{1,3})?(([Zz]$)|((?<offset>[\+|\-])(?<hours>[01][0-9]|2[0-3]):(?<minutes>[0-5][0-9])))
/xms;
# Default to UTC (when 'Z' is passed) for inbound timezone.
# The regex above succeeds for both 'z', 'Z' and '+/-' offset.
# We set tz as though Z was passed by default and then correct it later if an offset is detected
# by the presence fo the <offset> variable.
$tz = DateTime::TimeZone->new( name => 'UTC' );
}
elsif ( $date_format eq 'iso' or $date_format eq 'sql' ) {
# iso or sql format are yyyy-dd-mm[ hh:mm:ss]"
$regex = $fallback_re;
@ -164,7 +147,7 @@ sub dt_from_string {
)?
)?
}xms;
$regex .= $time_re unless ( $date_format eq 'rfc3339' );
$regex .= $time_re;
$fallback_re .= $time_re;
# Ensure we only accept date strings and not other characters.
@ -310,8 +293,7 @@ sub output_pref {
}
elsif ( $pref =~ m/^rfc3339/ ) {
if (!$dateonly) {
$date = $dt->strftime('%FT%T%z');
substr($date, -2, 0, ':'); # timezone "HHmm" => "HH:mm"
$date = Koha::DateTime::Format::RFC3339->format_datetime($dt);
}
else {
$date = $dt->strftime("%Y-%m-%d");

View File

@ -25,10 +25,12 @@ use Mojo::JSON;
use Scalar::Util qw( blessed looks_like_number );
use Try::Tiny qw( catch try );
use List::MoreUtils qw( any );
use DateTime::Format::MySQL;
use Koha::Database;
use Koha::DateTime::Format::RFC3339;
use Koha::DateTime::Format::SQL;
use Koha::Exceptions::Object;
use Koha::DateUtils qw( dt_from_string output_pref );
use Koha::Object::Message;
=head1 NAME
@ -423,10 +425,8 @@ sub TO_JSON {
elsif ( _datetime_column_type( $columns_info->{$col}->{data_type} ) ) {
eval {
return unless $unblessed->{$col};
$unblessed->{$col} = output_pref({
dateformat => 'rfc3339',
dt => dt_from_string($unblessed->{$col}, 'sql'),
});
my $dt = Koha::DateTime::Format::SQL->parse_datetime( $unblessed->{$col} );
$unblessed->{$col} = Koha::DateTime::Format::RFC3339->format_datetime($dt);
};
}
}
@ -832,19 +832,21 @@ sub attributes_from_api {
$value = ( $value ) ? 1 : 0;
}
elsif ( _date_or_datetime_column_type( $columns_info->{$koha_field_name}->{data_type} ) ) {
try {
if ( $columns_info->{$koha_field_name}->{data_type} eq 'date' ) {
$value = $dtf->format_date(dt_from_string($value, 'iso'))
if defined $value;
}
else {
$value = $dtf->format_datetime(dt_from_string($value, 'rfc3339'))
if defined $value;
if (defined $value) {
try {
if ( $columns_info->{$koha_field_name}->{data_type} eq 'date' ) {
my $dt = DateTime::Format::MySQL->parse_date($value);
$value = $dtf->format_date($dt);
}
else {
my $dt = Koha::DateTime::Format::RFC3339->parse_datetime($value);
$value = $dtf->format_datetime($dt);
}
}
catch {
Koha::Exceptions::BadParameter->throw( parameter => $key );
};
}
catch {
Koha::Exceptions::BadParameter->throw( parameter => $key );
};
}
$params->{$koha_field_name} = $value;

View File

@ -140,7 +140,7 @@ cmp_ok( $dt0->epoch(), 'eq', '1325455199', 'dt_from_string handles seconds with
eval {
$dt0 = dt_from_string( '2012-01-01T23:59:59.999Z+02:00', 'rfc3339' ); # Do not combine Z with +02 !
};
like( $@, qr/.*does not match the date format \(rfc3339\).*/, 'dt_from_string should die when passed a bad rfc3339 date string' );
like( $@, qr/Invalid date format/, 'dt_from_string should die when passed a bad rfc3339 date string' );
eval {
$dt0 = dt_from_string('2021-11-03T10:16:59Z+00:00', 'iso'); # Z and +00 are the same, but should not be together

View File

@ -0,0 +1,61 @@
#!/usr/bin/perl
use Modern::Perl;
use Test::More;
use Test::Exception;
BEGIN { use_ok('Koha::DateTime::Format::RFC3339'); }
subtest 'UTC datetime' => sub {
plan tests => 7;
my $dt = Koha::DateTime::Format::RFC3339->parse_datetime('2024-01-02T10:11:12Z');
is( $dt->year, 2024 );
is( $dt->month, 1 );
is( $dt->day, 2 );
is( $dt->hour, 10 );
is( $dt->minute, 11 );
is( $dt->second, 12 );
ok( $dt->time_zone->is_utc );
};
subtest 'with timezone' => sub {
plan tests => 7;
my $dt = Koha::DateTime::Format::RFC3339->parse_datetime('2024-01-02T10:11:12+01:30');
is( $dt->year, 2024 );
is( $dt->month, 1 );
is( $dt->day, 2 );
is( $dt->hour, 10 );
is( $dt->minute, 11 );
is( $dt->second, 12 );
is( $dt->time_zone->name, '+0130' );
};
subtest 'fractions of seconds are ignored' => sub {
plan tests => 8;
my $dt = Koha::DateTime::Format::RFC3339->parse_datetime('2024-01-02T10:11:12.34+01:30');
is( $dt->year, 2024 );
is( $dt->month, 1 );
is( $dt->day, 2 );
is( $dt->hour, 10 );
is( $dt->minute, 11 );
is( $dt->second, 12 );
is( $dt->nanosecond, 0 );
is( $dt->time_zone->name, '+0130' );
};
subtest 'invalid date throws an exception' => sub {
plan tests => 1;
throws_ok {
my $dt = Koha::DateTime::Format::RFC3339->parse_datetime('2024-01-02T10:11:12');
}
qr/Invalid date format/;
};
done_testing;

View File

@ -213,7 +213,7 @@ subtest 'TO_JSON tests' => sub {
(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))
/xms;
like( $updated_on, $rfc3999_regex, "Date-time $updated_on formatted correctly");
like( $lastseen, $rfc3999_regex, "Date-time $updated_on formatted correctly");
like( $lastseen, $rfc3999_regex, "Date-time $lastseen formatted correctly");
# Test JSON doesn't receive strings
my $order = $builder->build_object({ class => 'Koha::Acquisition::Orders' });