Name

Sub::Protected - Enforce protected subroutine access (Java/C++ semantics)

Version

0.03

Synopsis

package Foo;
use Sub::Protected;              # enables the :Protected attribute

sub new { bless {}, shift }

# Attribute form (preferred: protection lives next to the definition)
sub _helper :Protected {
    ...
}

sub public_method {
    my $self = shift;
    $self->_helper;              # OK -- same package
}

# ----------------------------------------------------------------

package Bar;
use Sub::Protected qw(_other _private);   # declarative form

sub _other   { 'other'   }
sub _private { 'private' }

Description

Enforces Java/C++-style "protected" access at runtime: a subroutine decorated with :Protected (or named in use Sub::Protected qw(...)) may only be called from within its defining package or from a subclass of that package. Any other caller causes a Carp::croak with a descriptive message.

Two Usage Forms

Bypass for Testing

Either condition alone (OR logic) disables all access checks:

$Sub::Protected::BYPASS is the recommended form for new test code; it is explicit and does not depend on the test runner. HARNESS_ACTIVE is a zero-config convenience.

The HARNESS_ACTIVE bypass can be disabled by setting:

$Sub::Protected::config{harness_bypass} = 0;

Configuration

The module exposes %Sub::Protected::config for runtime configuration:

The hash is compatible with Object::Configure for dependency-injection scenarios.

Error Message Format

_helper() is a protected method of Foo and cannot be called from Bar

The error is raised with "croak" in Carp, so it reports the file and line of the offending call, not a line inside Sub::Protected.

How Access Is Decided

Public Interface

Import

use Sub::Protected;                    # attribute form -- no arguments
use Sub::Protected qw(_a _b _c);      # declarative form

Purpose

Called automatically by use Sub::Protected.

With no arguments: does nothing beyond making the :Protected attribute globally available (which happens when the module is first loaded).

With one or more sub names: registers those subs in the calling package for wrapping at CHECK time. If CHECK has already fired (e.g. the module was loaded via runtime require, a plugin loader or a string eval), wrapping is applied immediately. Each named sub must be defined before CHECK fires (for pre-CHECK loads) or before import is called (for post-CHECK loads), so a module loaded at run time should call Sub::Protected->import(...) at the end of the file, after its subs are defined.

Run-time loading needs Perl 5.14 or later. On older Perls the module must be loaded at compile time (use, or require inside BEGIN).

Arguments

Returns

$class (the importing class name). The return value is ignored by the use mechanism; it is provided for optional method chaining at the class level.

Side Effects

Example

package Foo;
use Sub::Protected qw(_helper _init);

sub _helper { ... }   # will be protected
sub _init   { ... }   # will be protected

Api Specification

Input
# Params::Get::get_params / Params::Validate::Strict schema
{
    # class is the implicit first argument, set by Perl's 'use' mechanism
    subs => {
        type     => 'array',
        required => 0,
        each     => {
            type  => 'string',
            regex => qr/\A[_a-zA-Z]\w*\z/,
        },
    },
}
Output
# Return::Set schema
{
    type    => 'string',
    desc    => 'The importing class name ($class), for optional chaining.',
}

Messages

The following table lists every error or warning this method can produce.

Message                                     Meaning
----------------------------------------    -------------------------------------
"Sub::Protected->import: 'NAME' is not a    A sub name passed to import() failed
 valid Perl identifier"                      the identifier regex.  Use a name
                                             matching /\A[_a-zA-Z]\w*\z/.
                                             NAME is shown as '' when the
                                             name was undef or a reference.

"Sub::Protected: PKG::NAME is not defined"  The named sub was not found in the
                                             package stash at wrap time.  For
                                             pre-CHECK loads, ensure the sub is
                                             a compile-time named sub.  For
                                             post-CHECK/runtime loads, ensure
                                             the sub is defined before import().

Calling a protected sub from outside its package or subclasses croaks with the message shown in "Error message format".

Formal Specification

ValidName ≙ { n : seq CHAR | n matches /\A[_a-zA-Z]\w*\z/ }

┌─ Import ──────────────────────────────────────────
│ ΔRegistry
│ class? : Package ; caller? : Package
│ names? : seq SubName ; postCheck : 𝔹
│ result! : Package
├───────────────────────────────────────────────────
│ ∀ n ∈ ran names? • n ∈ ValidName
│ postCheck ⇒ (∀ n ∈ ran names? • defined(caller?, n))
│ postCheck ⇒ protected′ = protected ∪ { n : ran names? • (caller?, n) }
│ ¬postCheck ⇒ pending′ = pending ⁀ ⟨ n : names? • (caller?, n) ⟩
│ result! = class?
└───────────────────────────────────────────────────

-- Failure cases (croak, Registry unchanged):
--   ∃ n ∈ ran names? • n ∉ ValidName
--     ⇒ "Sub::Protected->import: 'n' is not a valid Perl identifier"
--   postCheck ∧ ∃ n ∈ ran names? • ¬defined(caller?, n)
--     ⇒ "Sub::Protected: caller?::n is not defined"

Known Limitations

Dependencies

Carp (core), Attribute::Handlers (core since 5.8), B::Hooks::EndOfScope, Readonly, Sub::Identify, Params::Get, Params::Validate::Strict, Return::Set.

See Also

Attribute::Handlers, Carp, Readonly, Params::Get, Params::Validate::Strict, Return::Set.

Formal Specification

Import

The following Z-notation schemas formally specify the state and operations of Sub::Protected. Unicode mathematical symbols are used in this section only.

-- Type abbreviations
Package  == seq CHAR     -- a non-empty Perl package name string
SubName  == seq CHAR     -- a Perl identifier string
Proc     == seq CHAR     -- abstract: a callable code reference

-- Ancestry relation (derived dynamically from @ISA chains)
anc : Package -> P Package
forall p : Package .
    anc p = {p} union bigcup { anc r | r in @ISA_of(p) }

-- Protected-access predicate
permitted : Package x Package -> BOOL
forall caller, owner : Package .
    permitted(caller, owner) <=> owner in anc(caller)

-- System state
+-Registry-------------------------------------------+
| protected : P (Package x SubName)                  |
| bypass    : BOOL                                   |
| config    : { harness_bypass : BOOL }              |
+----------------------------------------------------+

-- Initial state
+-InitRegistry---------------------------------------+
| Registry                                           |
|----------------------------------------------------|
| protected = {}                                     |
| bypass    = false                                  |
| config    = { harness_bypass |-> true }            |
+----------------------------------------------------+

-- Wrap: add a sub to the protected registry
+-Wrap-----------------------------------------------+
| Delta-Registry                                     |
| pkg? : Package ; name? : SubName                   |
|----------------------------------------------------|
| protected' = protected union { (pkg?, name?) }     |
| bypass'    = bypass                                |
| config'    = config                                |
+----------------------------------------------------+

-- Bypass predicate
bypass_active(R) <=>
    R.bypass or (R.config.harness_bypass and HARNESS_ACTIVE)

-- Access check: no state change
+-CheckAccess----------------------------------------+
| Xi-Registry                                        |
| caller? : Package                                  |
| owner?  : Package                                  |
| name?   : SubName                                  |
| ok!     : BOOL                                     |
|----------------------------------------------------|
| (owner?, name?) in protected                       |
| ok! <=> bypass_active or permitted(caller?, owner?)|
+----------------------------------------------------+

-- Violation (croak case):
--   not ok! =>
--   croak("name?()" ++ " is a protected method of " ++ owner?
--         ++ " and cannot be called from " ++ caller?)

Author

Nigel Horne, <njh at nigelhorne.com>

Copyright 2026 Nigel Horne.

Usage is subject to the GPL2 licence terms. If you use it, please let me know.