package Devel::Walk::Unstorable; use strict; use warnings; use Devel::Walk (); require Exporter; our @ISA = qw(Exporter); our %EXPORT_TAGS = ( 'all' => [ qw( unstorable ) ] ); our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); our @EXPORT = qw( unstorable ); our $VERSION = '0.02'; ############################################### sub unstorable { my( $obj, $name ) = @_; my $S = Devel::Walk::Unstorable->new; $S->walk( $obj, $name ); return $S->list; } ############################################### sub new { my $package = shift; return bless {@_}, $package; } sub walk { my( $self, $obj, $name ) = @_; local $self->{LAST}; $self->{list} ||= []; Devel::Walk::walk( $obj, sub { $self->__unstorable( @_ ); }, $name ); push @{ $self->{list} }, $self->{LAST} if defined $self->{LAST} and @{ $self->{list} } and $self->{list}[-1] ne $self->{LAST}; } sub list { @{ $_[0]->{list} } } sub __deepest { my( $self, $old, $new ) = @_; return unless defined $old; my $ol = length( $old ); # $old = '$something->{one}' # $new = '$something->{one}[0]' # $old is not deepest return if substr( $new, 0, $ol ) eq $old; # $old = '$something->{one}' # $new = '$${$something->{one}}{more}' # $old is not deepest return if $new =~ /^\$\$\{\Q$old\E\}/; # $old = '$something->{one}' # $new = '$something->{two}' # $old is deepest # warn "deepest='$old'"; return 1; } sub __unstorable { my( $self, $loc, $obj ) = @_; require 'Storable.pm'; if( $self->__deepest( $self->{LAST}, $loc ) ) { # warn "deepest=$self->{LAST}"; push @{ $self->{list} }, $self->{LAST}; $self->{LAST} = undef; } if( $self->check( $loc, $obj ) ) { # warn "$loc is unstorable"; $self->{LAST} = $loc; return 1; } else { # warn "$loc is storable"; } return; } sub check { my( $self, $loc, $obj ) = @_; return 1 if ref( $obj ) and not eval { $SIG{__DIE__} = 'DEFAULT'; Storable::freeze( $obj ); 1; }; return; } 1; __END__ =head1 NAME Devel::Walk::Unstorable - Find locations in complex structures that can't be serialized with Storable. =head1 SYNOPSIS use Devel::Walk::Unstorable; my @list = unstorable( $suspect, '$suspect' ); die "Can't store ", join "\n", @list if @list; =head1 DESCRIPTION This module uses L to find all the locations of objects that can't be stored with L. If you are like me, you regularly try to serialize large objects and save them in a session file for your web application. Storable's freeze is ideal for this, except when it isn't. You forgot to close a DBI handle somewhere deep in your object. Storable just reports this as a CODE reference, but doesn't tell you what part of your structure is holding that reference. You can use C to walk your object structure to find the location. It is highly recomended to only do this in a development environment, and only if freeze has failed. my $data = eval { freeze( $obj ) }; if( $@ ) { warn $@; my @list = unstorable( $obj, '$obj' ); die "Unstorable reference at ", join "\n", @list; } # now you can write $data to you session DB. =head2 unstorable my @bad = unstorable( $obj, $basename ); Walks C<$obj> and finds all the locations that can't be stored with L. Returns the list of locations, if any. C<$basename> may be omited and defaults to C<'$o'>. =head1 OBJECT You might want to customize the behaviour of this module. If so, you can subclass C and overload one of the worker modules. my $walker = Devel::Walk::Unstorable->new; $walker->walk( $obj, $name ); my @bad = $walker->list; =head2 new my $walker = Devel::Walk::Unstorable->new; =head2 walk $walker->walk( $struct, '$basename' ); Recursely walks through C<$suspect>, invoking L on each element of each structure. C<$basename> is updated with each recursion to be. Only walks through ARRAY, HASH, SCALAR and REF references. All locations that fail L will be accumulated in a list. The list will only contain the deepest locations. If C<$basename> is empty, C<'$o'> is used as a default. =head2 list my @bad = $walker->list; Returns the list of location that failed L. =head2 check my $ok = $walker->check( $loc, $obj ); By default, C will return true if C<$obj> is a reference that fails to be serialized with L. Returns false if C<$obj> is not a reference, or if it's a reference that can be sucessfully serialized with L. =head1 SEE ALSO L L =head1 AUTHOR Philip Gwyn, Eperl -at- pied.nuE =head1 COPYRIGHT AND LICENSE Copyright (C) 2025 by Philip Gwyn This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.26.3 or, at your option, any later version of Perl 5 you may have available. =cut