lordfireen / friends
Allows classes to be friends with each other and access protected and private properties and methods and constants
Requires
- php: ^8.2
Requires (Dev)
- lordfireen/checkpoint: *
- phpunit/phpunit: ^11.5
This package is auto-updated.
Last update: 2026-05-26 16:56:25 UTC
README
Package that allows you to create friend classes/functions in PHP.
Installation
Use composer to install the library.
composer require lordfireen/friends
Usage
Defining Friendship
To define a friendship, you need to use the Friend attributes:
LF\Friends\Attributes\FriendClass- for class-based friendshipsLF\Friends\Attributes\FriendFunction- for function-based friendships
The attributes could be attached to:
class- Then the friend class/function will gain access to all protected and private members.method- Then the friend class/function will gain access to that method.property- Then the friend class/function will gain access to that property.constant- Then the friend class/function will gain access to that constant.
Trait:
You also need to use the LF\Friends\Accessor\Friends trait in class that you want to have friends.
Example:
use LF\Friends\Attributes\{FriendClass, FriendFunction};
use LF\Friends\Accessor\Friends;
#[FriendClass(SuperFriend::class)]
class User
{
use Friends;
protected const MY_CONSTANT = 'Hello there!';
protected static $myStaticProperty = 'I can see you';
#[FriendClass(MyFriendClass::class)]
private int $privateProperty = 15;
#[FriendFunction('my_friend_function')]
private function some_method(string $name) {
echo 'Hello ' . $name;
}
protected static function some_static_method() {
echo 'Hello static';
};
}
Friendship defined in this way in the User class will allow:
SuperFriendclass to access protected and private methods, properties and constantsMyFriendClassclass to access to theprivatePropertyproperty.my_friend_functionfunction to access thesome_methodmethod.
Accessing protected and private members:
For properties and methods, you can access them using an object like normal because Trait Friends uses magic methods so you could simply do:
class SuperFriend
{
public function callFriend() {
$user = new User();
$user->privateProperty = 20;
echo $user->privateProperty; // 20;
$user->some_method('friend'); // Hello friend
User::some_static_method(); // Hello static
}
}
But unfortunately, you can't access constants and static properties like normal, because php doesn't have magic methods for them.
But don't worry, you can still access them using the accessor method ::friend().
That will return the accessor object that you can use to access the protected and private const and static properties.
class SuperFriend
{
public function otherMethod() {
User::friend()->const('MY_CONSTANT'); // Hello there!
if (User::friend()->isset('myStaticProperty') {
echo User::friend()->get('myStaticProperty'); // I can see you
}
User::friend()->set('myStaticProperty', 'I can change you');
echo User::friend()->get('myStaticProperty'); // I can change you
}
}
What if I don't have access?
If a class/function is not friend and try to access protected/private member of a class, then a AccessDenied exception will be thrown.
function not_friend_function()
{
User::friend()->const('MY_CONSTANT'); // Throws LF\Friends\Exceptions\AccessException\ConstantAccessDenied
}
The same happens if you try to access a protected/private member directly via a magic method.
$user = new User();
$user->privateProperty = 20; // Throws LF\Friends\Exceptions\AccessException\PropertyAccessDenied
How does it work?
- First the
Friendstrait adds a magic method__getetc. to the class. - Then if property/method is protected/private then PHP launch magic method
__getetc. - Inside each magic method first it checks who calls it (via
debug_backtracefunction). - Then it checks if the caller is a friend (via
MemberAccessCheckerinstance). - If it is, then it calls the class method/property; if not, then throws an exception.
public function __get(string $name): mixed
{
$caller = CallerInspector::getCaller();
if (!self::checker()->checkProperty($caller, $name)) {
throw new PropertyAccessDenied(class: static::class, property: $name, caller: $caller);
}
return $this->$name;
}
private static function checker(): MemberAccessChecker
{
return self::$checker ??= new DefaultMemberAccessChecker(static::class);
}
For static properties and constants it is similar, but first you need to get StaticFriendAccessor instance:
public static function friend(): StaticFriendAccessor
{
self::$accessor ??= new StaticFriendAccessor(static::class, self::checker());
self::$accessor->setCaller(CallerInspector::getCaller());
return self::$accessor;
}
DefaultMemberAccessChecker
DefaultMemberAccessChecker is responsible for checking if a class/function is a friend or not.
It uses two objects to help itself:
FriendsRegister- which stores all friendship defined in the class.MemberInvocabilityChecker- which inspects if the class/function is even invokable. For example, a method must exist, and you can't access it when it's abstract.
The DefaultMemberAccessChecker by default uses DefaultFriendsRegister and DefaultMemberInvocabilityChecker
but you can use your own implementation of FriendsRegister and MemberInvocabilityChecker interfaces.
For example, DefaultFriendsRegister use simple array to store friendship, but if you want to use a database, redis, etc. you can implement your own FriendsRegister interface.
The same goes for DefaultMemberInvocabilityChecker which checks via Reflection if the class/function is invokable and also stores the invokability status in simple an array to avoid checking it again.
It is fast?
To be honest, no... but also yes.
The most expensive part is checking via debug_backtrace function who calls the class.
And this is a thing that cannot be stored because it must be done every time when you try to access a protected/private member.
But by default implementation of each interface, the Friendship is stored; the invokability status is stored, so the checking is very fast itself.
I created a benchmark (You could see it in tests\Integration\benchmark.php) to simply test it and compare with normal access
This is the result running on my computer:
10 000 000 times:
For properties
time for friend access total: 84.405397
time for friend access per 1 access: 0.844 µs
time for normal access: 0.829719
time for normal access per 1 access: 8.297 ns
Access by friendship is X times longer:: 101.72768973592
For constants
time for friend access total: 106.359060
time for friend access per 1 access: 1.064 µs
time for normal access: 0.573153
time for normal access per 1 access: 5.732 ns
Access by friendship is X times longer:: 185.5683560934
For methods
time for friend access total: 87.187268
time for friend access per 1 access: 0.872 µs
time for normal access: 1.006057
time for normal access per 1 access: 10.061 ns
Access by friendship is X times longer:: 86.662354121089
And for
1 000 000 times:
For properties
time for friend access total: 1.061319
time for friend access per 1 access: 1.061 µs
time for normal access: 0.008040
time for normal access per 1 access: 8.040 ns
Access by friendship is X times longer:: 132.00485074627
For constants
time for friend access total: 1.030990
time for friend access per 1 access: 1.031 µs
time for normal access: 0.006152
time for normal access per 1 access: 6.152 ns
Access by friendship is X times longer:: 167.58615084525
For static methods
time for friend access total: 0.873577
time for friend access per 1 access: 0.874 µs
time for normal access: 0.010224
time for normal access per 1 access: 10.224 ns
Access by friendship is X times longer:: 85.443759780908
So, as you can see, the friendship is slow compared to normal access (80-100 times slower, and for some reason 130-180 times slower for constants), but to be fair, it is still fast enough for most cases.
Let's be honest, on average it's less than 1 µs per access. In 1 second I can access 1 000 000 times each member, so for me, it's not a big deal.
Of course, the benchmark is pretty simple, but I guess for real world application the result could be pretty simple if you don't use millions of friends. And of course it depends on the machine you are running on.
But to sum up, I believe that for a reasonable use, it is perfectly possible to use it in a production environment.
Too slow, can it be sped up?
Yes, I think it is possible.
- You could replace the
CallerInspector::getCaller()via debug_backtrace and thanks to this you will be able to shorten the stack by 1 frame, which I guess will significantly speed up the execution. - You could use other cache to store the friendship and invokability status like redis. Maybe this will improve the performance between requests. Because from now on, each request needs to check the friendship and invokability status ONCE per member.
- You could use a different implementation of
MemberAccessCheckerinterface. Maybe you will be able to improve the performance even more.
So why don't I do it myself? Personally, I find the speed satisfactory, and whether I can do 1.3 million request checks instead of 1 million isn't a big deal.
Is it safe?
I think so.
Someone might think that they could replace the Caller and set it as a different object, for example, as an actual friend, and thus gain access to private properties... And they would be right.
However, they could also do it using Reflection and the setValue method, or simply by going into the file and changing private to public.
For someone who wants to bypass encapsulation, it would still be possible, with or without friendship.
What if I already use magic methods like __get and still wanted to use friendship? 😭
Relax, you can still use friendship.
Admittedly, this will involve writing your own methods for calling the friendship.
In such a scenario, if you use all the magic methods, you'll essentially be writing the exact same thing in them as in the Friends Trait.
But if you only use one magic method, like __get(), you can add the Trait to the class as follows:
class User
{
use Friends {
Friends::__get as private __friendGet;
};
public function __get(string $name): mixed
{
# Your logic, what to do. First check the friendship, or do something else?
if ($name === 'hiddenPath') {
# Do something
} else {
$caller = CallerInspector::getCaller();
if (!self::checker()->checkProperty($caller, $name)) {
throw new PropertyAccessDenied(class: static::class, property: $name, caller: $caller);
}
return $this->$name;
// NEVER call __friendGet() method directly. Because the CallerInspector::getCaller() inside will returns User::__get() as the caller.
}
}
}
Extra
I've decided that trying to access a constant or static property using an Accessor is a flawed approach. Therefore, in this scenario:
class User
{
use Friends;
#[FriendClass(Friend::class)]
public const MY_CONSTANT = 'Hello there!';
}
class Friend
{
public static function callFriend() {
User::friend()->const('MY_CONSTANT');
}
}
Friend::callFriend();
an AlreadyAccessible exception will be thrown.
It's possible to disable it by setting the flag $allowAlreadyAccessible = true in the DefaultMemberAccessChecker object,
and then you could access the accessor, but I don't recommend this, and an E_USER_WARNING error would be triggered,
that says that public members should not be accessed in this way.
What about friendship with a Trait or Enum?
Yes, you could use friendship with an Enum like that:
#[FriendClass(MyEnum::class)]
class Friend
{
use Friends;
protected string $name = 'Friend';
}
enum MyEnum: string
{
case A = 'Best friend';
case B = 'Some guy';
public function callFriend(Friend $friend)
{
$friend->name = $this->value;
}
}
But Trait is not supported. You need to add the Friendship to the class that uses the Trait. Maybe in the future I will add support for Traits.
But what about the Enum, Trait having friends?
For enum it's not possible since the Enum cannot have magic methods, there is no way to access the Enum members like normal. However, maybe in the future I will add support for Enums in other ways.
For Trait, it's possible, but you need to add the Friendship to the member, you can't add it directly to the whole Trait. Maybe in the future I will add support for Traits.