DBIx-QuickORM
view release on metacpan or search on metacpan
worktrees/audit-fixes-master/lib/DBIx/QuickORM/Schema/Autofill.pm view on Meta::CPAN
package DBIx::QuickORM::Schema::Autofill;
use strict;
use warnings;
our $VERSION = '0.000028';
use Carp qw/croak/;
use DBIx::QuickORM::Util qw/load_class/;
# A schema is autofilled twice against the same generated row class: once for
# the introspected schema and again for the merged (introspected + declared)
# schema. Column accessors must be remembered across both passes so that a
# relationship declared only in the merged schema is still caught when it
# collides with a column accessor installed by the introspected pass. The
# registry is kept per-autofill-instance (see define_autorow) rather than in a
# package global, so it does not leak claims between two independent builds
# that happen to reuse the same explicit autorow base class.
use Object::HashBase qw{
<types
<affinities
<hooks
+skip
<no_volatile
};
=pod
=encoding UTF-8
=head1 NAME
DBIx::QuickORM::Schema::Autofill - Autofill configuration for schema introspection.
=head1 DESCRIPTION
Holds the type maps, affinity callbacks, and hooks used while autofilling a
schema from a live database. It maps introspected SQL types to
L<DBIx::QuickORM::Type> classes, runs user-supplied hooks at well-known points,
and generates field and link accessors on autovivified row classes.
=head1 SYNOPSIS
my $autofill = DBIx::QuickORM::Schema::Autofill->new(
types => {...},
affinities => {...},
hooks => {...},
);
$autofill->define_autorow($row_class, $table);
=head1 ATTRIBUTES
=over 4
=item types
Hashref mapping SQL type names to type objects/classes.
=item affinities
Hashref mapping affinity names to arrayrefs of callbacks.
=item hooks
Hashref mapping hook names to arrayrefs of callbacks.
=item skip
Nested hashref describing what to skip during autofill.
=back
=cut
# Maps each valid hook name to its seed key: the args key whose value is
# threaded through the callbacks registered for that hook.
my %HOOKS = (
column => 'column',
columns => 'columns',
index => 'index',
indexes => 'indexes',
links => 'links',
post_column => 'column',
post_table => 'table',
pre_column => 'column',
pre_table => 'table',
primary_key => 'primary_key',
table => 'table',
tables => 'tables',
unique_keys => 'unique_keys',
link_accessor => 'name',
field_accessor => 'name',
);
=pod
=head1 PUBLIC METHODS
=over 4
=item $bool = $autofill->is_valid_hook($name)
True if C<$name> is a recognized hook name.
=cut
sub is_valid_hook {
my ($self, $hook) = @_;
return $HOOKS{$hook} ? 1 : 0;
}
=pod
=item $out = $autofill->hook($name, \%args, $seed)
Run every callback registered for the named hook as a pipeline. Each hook has
a designated seed key in C<\%args> (for example C<table> for the table hooks,
C<name> for the accessor hooks). Every callback is called with the args (plus
C<autofill>) with the running value under the seed key, and its return value
becomes the running value passed to the next callback and ultimately returned.
A callback that modifies the seed in place must still return it so the
callbacks after it (and the caller) see the same value. The pipeline starts
from C<$seed> when given, otherwise from the seed key's value in C<\%args>;
with a single registered callback this matches the old single-callback
behavior exactly.
=cut
sub hook {
my $self = shift;
my ($hook, $args, $seed) = @_;
croak "'$hook' is not a valid hook" unless $HOOKS{$hook};
my $key = $HOOKS{$hook};
my $out = @_ > 2 ? $seed : $args->{$key};
for my $cb (@{$self->{+HOOKS}->{$hook} // []}) {
$args->{$key} = $out;
$out = $cb->(%$args, autofill => $self);
}
return $out;
}
=pod
=item $val = $autofill->skip(@path)
Walk the nested C<skip> hashref along C<@path>, returning the value found or
false (0) as soon as any step is missing.
=cut
sub skip {
my $self = shift;
my $from = $self->{+SKIP};
while (@_) {
my $arg = shift @_;
$from = $from->{$arg} or return 0;
}
return $from;
}
=pod
=item $autofill->process_column(\%col)
Resolve the column's scalar-ref type into a real type object, using the type map
first and then affinity callbacks. Updates the column's C<type> and C<affinity>
in place when a match is found.
=cut
sub process_column {
my $self = shift;
my ($col) = @_;
my $type = $col->{type};
my $tref = ref($type);
return unless $tref && $tref eq 'SCALAR';
my $new_type;
$new_type = $self->{+TYPES}->{$$type} // $self->{+TYPES}->{uc($$type)} // $self->{+TYPES}->{lc($$type)};
unless ($new_type) {
if (my $aff = $col->{affinity}) {
if (my $list = $self->{+AFFINITIES}->{$aff}) {
for my $cb (@$list) {
$new_type = $cb->(%$col) and last;
}
}
}
}
return unless $new_type;
$col->{type} = $new_type;
$col->{affinity} = $new_type->qorm_affinity(sql_type => $$type);
}
=pod
=item $autofill->define_autorow($row_class, $table)
Load (or autovivify) the row class, then install field accessors for each column
and link accessors for each link, honoring the C<field_accessor> and
C<link_accessor> hooks.
An accessor that already exists on the row class is left untouched. Each
generated accessor name must be unique on the row class; if two relationships,
or a relationship and a column, resolve to the same name this croaks and names
the conflict. Names are resolved after any C<aliases> and
C<link_accessor>/C<link> hooks, so those hooks resolve a collision: give the
relationships distinct aliases, or a name hook that returns a distinct name for
each.
=cut
sub define_autorow {
my $self = shift;
my ($row_class, $table) = @_;
unless(load_class($row_class)) {
my $err = $@;
die $err unless $err =~ m/Can't locate.*in \@INC/;
my $row_file = $row_class;
$row_file =~ s{::}{/}g;
$row_file .= ".pm";
$INC{$row_file} = __FILE__;
( run in 0.620 second using v1.01-cache-2.11-cpan-85f18b9d64f )