The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package Object::Stash;

use 5.010;
use strict;
use utf8;

BEGIN {
	$Object::Stash::AUTHORITY = 'cpan:TOBYINK';
	$Object::Stash::VERSION   = '0.005';
}

use Carp qw/croak/;
use Hash::FieldHash qw/fieldhashes/;
use Scalar::Util qw/blessed/;
use Sub::Name qw/subname/;

my %known_stashes;
my %Stashes;
BEGIN {
	fieldhashes \%known_stashes, \%Stashes;
}

sub import
{
	my ($invocant, @args) = @_;
	
	my %args;
	while (defined(my $arg = shift @args))
	{
		if ($arg =~ /^-/)
		{
			$args{$arg} = shift @args;
		}
		else
		{
			push @{$args{-method}}, $arg;
		}
	}
	$args{-method} //= ['stash'];
	$args{-type}   //= 'hashref';
	
	croak sprintf("Stash type '%s' is unknown.", $args{-type})
		unless $args{-type} =~ m{^ hashref | object $}ix;
	
	my $caller = $args{-package} // caller;
	
	for my $method (@{$args{-method}})
	{
		no strict 'refs';
		my $name = "$caller\::$method";
		*$name = my $ref = subname($name, sub { unshift @_, $name, lc $args{-type}; goto &_stash; });
		$known_stashes{ $ref } = $name;

		if (lc $args{-type} eq 'object')
		{
			my $name_autoload = $name . '::AUTOLOAD';
			my $autoload = sub :lvalue
			{
				my ($func) = (${$name_autoload} =~ /::([^:]+)$/);
				my $self = shift;
				$self->{$func} = shift if @_;
				$self->{$func};
			};
			*$name_autoload = subname($name_autoload, $autoload);			
		}
	}
}

sub is_stash
{
	shift if (!ref $_[0] and $_[0]->isa(__PACKAGE__));
	my ($name) = @_;
	
	return $known_stashes{ $name } if exists $known_stashes{ $name };
	return;
}

{	
	sub _stash
	{
		my ($stashname, $type, $self, @args) = @_;
		
		my (%set, @retrieve);
		if (scalar @args == 1 and ref $args[0] eq 'HASH')
		{
			%set = %{ $args[0] };
		}
		elsif (scalar @args == 1 and ref $args[0] eq 'ARRAY')
		{
			@retrieve = @{ $args[0] };
		}
		elsif (scalar @args % 2 == 0)
		{
			%set = @args;
		}
		elsif (@args)
		{
			croak "$stashname expects to be passed a hash, hash reference, or nothing.";
		}
		
		return unless (defined wantarray or @args);
		
		my $stash = $Stashes{ $self }{ $stashname };
		unless (defined $stash)
		{
			$stash = $Stashes{ $self }{ $stashname } 
			       = ($type eq 'object' ? (bless {}, $stashname) : {});
		}
			
		while (my ($k, $v) = each %set)
		{
			$stash->{$k} = $v;
		}
		
		if (@retrieve)
		{
			my @return = map { $stash->{$_} } @retrieve;
			return wantarray ? @return : \@return;
		}
		
		return $stash;
	}
}

'Secret stash';

__END__

=head1 NAME

Object::Stash - provides a Catalyst-like "stash" method for your class

=head1 SYNOPSIS

 {
   package MyClass;
   use Object::New;
   use Object::Stash 'data';
 }
 
 use feature 'say';
 use Data::Printer qw(p);
 my $obj = MyClass->new;
 p $obj->data;                     # an empty hashref
 $obj->data(foo => 1, bar => 2);   # sets values in the 'data' stash
 $obj->data({foo => 1, bar => 2}); # same
 p $obj->data;                     # hashref with keys 'foo', 'bar'
 say $obj->data->{foo};            # says '1'
 
 # Retrieve multiple values
 my @values = $obj->data(['foo', 'bar']);
 say $values[0];                   # says '1'
 say $values[1];                   # says '2'
 
 # Or in scalar context
 my $values = $obj->data(['foo', 'bar']);
 say $values->[0];                 # says '1'
 say $values->[1];                 # says '2'

=head1 DESCRIPTION

The L<Catalyst> context object has a method called stash, that provides a
hashref for storing arbitrary data associated with the object. This is
arguably a little hackish - the proper solution might be to create a slot
for each piece of information you wish to store, with appropriate accessor
methods. But often hackish will do.

(And there are non-hackish ways of using Object::Stash. Take a look at
L<Web::Magic> which uses a private stash - named with a leading underscore -
and provides public methods for accessing various things stored inside it.)

Object::Stash sets up one or more stash methods for your class. How these
methods are named depends on how Object::Stash is imported. Object::Stash
is a role, like L<Object::New> or L<Object::ID>. This means you import it,
but don't inherit from it.

=head2 Default method name

 package MyClass;
 use Object::Stash;

Creates a single method for MyClass objects. The method is called "stash".

=head2 Custom method name

 package MyClass;
 use Object::Stash 'data';

Creates a single method for MyClass objects. The method is called "data".

=head2 Multiple methods

 package MyClass;
 use Object::Stash qw/important trivial/;

Creates two stashes for MyClass objects, called "important" and "trivial".
Adding data to one stash will not affect the other stash. You could
alternatively write:

 package MyClass;
 use Object::Stash 'important'
 use Object::Stash 'trivial';

=head2 Adding stashes to other classes

 package MyClass;
 use Object::Stash -package => 'YourClass', 'my_stash';

Creates a stash called "my_stash" for YourClass objects.

=head2 Utility Functions

=over

=item C<< Object::Stash->is_stash( $coderef ) >>

Returns true if the method is a stash. For example:

  my $method = MyClass->can('trivial');
  if (Object::Stash->is_stash($method))
  {
    $method->(foo => 1, bar => 2);
  }

Can also be called as C<< Object::Stash::is_stash($coderef) >>.

=back

=head2 Stash Storage

Stashes are stored "inside-out", meaning that they will work not only with
objects which are blessed hashrefs, but also with any other type of object
internals. Dumping your object with L<Data::Dumper> or similar will not
display the contents of the stashes. (A future release of this module may
introduce other storage options, but the current inside-out storage is
likely to remain the default.)

Thanks to L<Hash::FieldHash>, an object's stashes I<should> get automatically
garbage collected once the object itself is destroyed, unless you've
maintained your own references to the stashes.

=head2 Stash Objects

While stashes are usually hashrefs, there is also an option to make stashes
themselves blessed objects. It's best to illustrate this with an example

 {
   package MyClass;
   use Object::New;
   use Object::Stash 'data', -type => 'object';
 }
 
 # All this stuff from SYNOPSIS still works...
 use feature 'say';
 use Data::Printer qw(p);
 my $obj = MyClass->new;
 p $obj->data;                     # an empty hashref
 $obj->data(foo => 1, bar => 2);   # sets values in the 'data' stash
 $obj->data({foo => 1, bar => 2}); # same
 say $obj->data->{foo};            # says '1'
 
 my @values = $obj->data(['foo', 'bar']);
 say $values[0];                   # says '1'
 say $values[1];                   # says '2'
 
 my $values = $obj->data(['foo', 'bar']);
 say $values->[0];                 # says '1'
 say $values->[1];                 # says '2'
 
 # But now you can retrieve data using accessor methods...
 say $obj->data->foo;              # says '1'
 
 # The accessors work as not just getters, but setters...
 $obj->data->foo(99);
 
 # The accessors can be treated as lvalues...
 $obj->data->foo = 100;
 $obj->data->foo++;
 
 # Cool, huh?
 say $obj->data->{foo};            # says '101'
 
 # In case you were wondering...
 say ref $obj->data;               # says 'Object::Stash::data'

=head1 BUGS

Please report any bugs to
L<http://rt.cpan.org/Dist/Display.html?Queue=Object-Stash>.

=head1 SEE ALSO

L<Object::New>, L<Object::ID>.

=head1 AUTHOR

Toby Inkster E<lt>tobyink@cpan.orgE<gt>.

=head1 COPYRIGHT AND LICENCE

This software is copyright (c) 2011 by Toby Inkster.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=head1 DISCLAIMER OF WARRANTIES

THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.