++ed by:
1 non-PAUSE user
Author image Αριστοτέλης Παγκαλτζής


Object::Properties - minimal-ceremony class builder


version 1.002


 package SomeClass;
 use Object::Properties qw( foo +bar ), '+baz' => \&_check_baz;
 sub _check_baz {
     my ( $self, $value ) = @_;
     ref $value ? croak 'SomeClass->baz must not be a ref' : $value;

Meanwhile, elsewhere:

 my $obj = SomeClass->new( foo => 7 );
 say $obj->foo;  # outputs 7
 $obj->foo = 42; # dies -- cannot be assigned to
 $obj->bar = 42; # no problemo; and any value goes
 say $obj->bar;  # outputs 42
 $obj->baz = ''; # no problemo encore -- except:
 $obj->baz = \1; # nope, _check_baz croaks


This is a class builder with a minimal API that can be used as a drop-in upgrade for Object::Tiny. It adds support support for field validation and read-write fields, realised as lvalue methods. Validation for lvalue writes will be efficient in XS-capable environments but will still function, slowly, in other situations.


Declaring properties

The module's import method accepts a list of field names and sets up an accessor for each of them in the package it was invoked from:

 use Object::Properties qw( foo bar );

Fields are read-only by default but you can request read-write fields by preceding them with a plus sign:

 use Object::Properties qw( readonly +readwrite );

By default, fields accept any value whatsoever, but any field name (read-only or read-write) may be followed by a reference to a validation function:

 use Object::Properties '+hostname' => \&_munge_hostname;

Any write to such a field will invoke its validation function, with the object instance and the new value for the field as its arguments. The return value of this function will then be stored as the field value:

 sub _munge_hostname { lc $_[1] }

You can return an empty list from a validation function, in which case nothing will be stored at all. This allows you to take over the entire handling of the value if needed. (You can also return more than one value, in which case only the first value will be stored, so that's generally a silly thing to do.)

Constructing instances

If you declare any validated fields, a method called PROPINIT will be added to your package along with the accessors. When called, it clears the values of whatever validated fields may already have been stored in the instance, then sets them from a hashref it expects to be passed as its only parameter. It also deletes every key it has used from this hashref:

 my $arg = { page => 5, max_page => 20 };
 $self->PROPINIT( $arg );
 # now $arg is {} and $self->page == 5 and $self->max_page == 20

It is valid to pass the instance itself as its parameter hash:

 my $self = bless { page => 5, max_page => 20 }, $class;
 $self->PROPINIT( $self );

You will probably want to call all the PROPINITs in your inheritance chain from your constructor (with aid from NEXT):

 sub new {
     my $class = shift;
     my $self = bless { @_ }, $class;
     $self->EVERY::LAST::PROPINIT( $self );
     return $self;

But if that is all your constructor would do, you will not need to write it: Object::Properties::Base contains such a new method and will be added to your @ISA as your superclass if that is empty.

Inter-field depencencies

NOTE that you have to take care of data dependencies. During construction, validated fields will be set in the order you declare them, so be sure that none of your validation functions depend on validated fields listed after them in the declaration order:

 use Object::Properties
     '+max_page' => \&_munge_max_page,
     '+page'     => \&_munge_page;

 # make sure it's always a number
 sub _munge_page {
     my ( $self, $value ) = @_;
     no warnings 'numeric';
     $value > $self->max_page ? croak 'Cannot go past last page' : 0+$value;

 sub _munge_max_page {
     my ( $self, $value ) = @_;
     { no warnings 'numeric'; $value = 0+$value; }
     croak 'Cannot shrink below current page' if $value < ( $self->page // 0 );

In this example, max_page is declared first and the function is written to deal with the case of page being uninitialized. Then, page can just depend on max_page already being initialized.

Reversing the order of declarations here would make it impossible to properly construct a new object with an initial page value – or to construct one at all, almost. It would always cause a warning due to max_page being undefined during the comparison in _munge_page, and any value other than 0 would trigger the croak.



Aristotle Pagaltzis <pagaltzis@gmx.de>


This software is copyright (c) 2016 by Aristotle Pagaltzis.

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