0

Sorry if this has been asked before but Googling didn't give it to me.

How do you protect methods (or properties) from only unrelated classes, but have it available for specific, related classes?

class supplier {
    protected person $contactPerson;
}

class unrelated {
    function unrelatedStuff(supplier $supplier) {
        $noneOfMyBusiness = $supplier->contactPerson; // should generate error
    }
}

class order {

    function __construct(readonly public supplier $supplier) {}

    function informAboutDelivery() {
         $contact = $this->supplier->contactPerson; 
         // should work (but doesn't) since to process an order you need the suppliers details
         $contact->mail('It has been delivered');
    }
}

I wrote a trait that can allow access to certain predefined classes to certain methods, whilst disallowing all other access, with a __method magic method and attributes. I can also think of using Reflection. But I feel that's all overcomplicated and that I am missing something quite obvious here. Many classes are very closely related, others are unrelated. What is the normal way to deal with this? How do you shield information but not from all classes?

3
  • There's nothing simple that does this. It sounds like you're looking for something like C++'s friend feature, but it doesn't exist in PHP. Methods are either public (accessible everywhere), private (only accessible in the class itself), or protected (accessible in the class and subclasses). Commented May 3, 2024 at 18:07
  • What about something like Facade pattern to hide business logic? Commented May 3, 2024 at 18:09
  • @Marcus The example is purely fictional, in reality it has nothing to do with business logic, but I run into it everywhere. I keep on making properties or methods public because very closely related classes need access to it, but for the rest I would want them to be private, Commented May 5, 2024 at 12:42

2 Answers 2

1

What about using a magic method with a backtrace?


class supplier {

    private $_contact;

    //DEFINE THE CLASSES THAT ARE ALLOWED TO ACCESS PROPERTIES
    private $_allowedClasses = [
        'contact' => [
            order::class,
            //Another::class
        ],
        'otherProperty' => [
            //Another::class
        ]
    ];

    public function __get($name){
        
        //DEFINE THE PREFIXED NAME
        $prefixed = '_'.$name;

        //CHECK IF THE ATTRIBUTE EXISTS
        if(isset($this->$prefixed){

            //GET THE BACKTRACE EFFICIENTLY
            $trace = debug_backtrace(2);

            //CHECK IF THE CALLING CLASS IS ALLOWED
            if(isset($this->_allowedClasses[$name] && isset($trace[1]['class']) && !in_array($trace[1]['class'])){
                throw new \Exception("{$trace[1]['class]} is not allowed to access this");
            }

            //SEND BACK THE VALUE
            return $this->$prefixed;
        }
    }
}

class unrelated {
    public function unrelatedStuff(supplier $supplier){
        return $supplier->contact; //THROWS AN ERROR BECAUSE THIS CLASS CANT CALL THE contact PROPERTY
    }
}

class order {

    protected $supplier;

    public function __construct(supplier $supplier){

        //SET SUPPLIER
        $this->supplier = $supplier;
    }
    
    public function informAboutDelivery(){
        //GET THE CONTACT (IF THIS CLASS IS NOT ALLOWED AN ERROR WILL BE THROWN)
        $contact = $this->supplier->contact;

        //SEND YOUR EMAIL
        $contact->mail("Some message");
    }
}
Sign up to request clarification or add additional context in comments.

4 Comments

Thank you. As stated. I have a trait that does something very similar, just with attributes to make a more general solution. The attributes provide the classes to render access too, so you can define those per per property. My problem with it is that it is cumbersome and it feels there should be a very simple solution to something that feels so natural.
You could easily use the code I provided and make a trait... then all you would have to do on any class is define the $_allowedClasses property on any class you want to setup restrictions on. You could simplify further by having all your model classes extend a single "baseModel" class and apply the trait to the baseModel. That would solve all of the issues you outlined in your post without having to copy code every time.
That’s absolutely possible and quite a good solution. However personally I would prefer not using __get and having the same names for the property and it’s definition since most editors link usage and definition to each other only if the names are the same. I guess though, that in PHP 8.4 those objections will be obsolete since we will have property hooks. You would just need a property hook that will call your checking function from the trait. Just a few months of patience needed…
Btw that would also simplify my own (stricter) solution just as drastically.
1

Another solution (still not very elegant, but more elegant than magic methods) that would work in quite a few cases, also restricts access to orders only for this particular supplier (which is more strict but that's good in this case), could be:

class supplier {
    protected person $contactPerson;

    function contact(order $caller): person {
        if ($caller->supplier === $this) return $this->contactPerson;
        throw new \Exception("only orders to this supplier may access the contact information");
    }        
}

This returns the supplier only if called from the class orders as such:

$supplier->contact(caller: $this);

The problem that I have with it, is that you will have to copy the code every time you want to use it, it's not a structural solution.

So I moved on to a more structural solution. This is the best I found (could also be done with implements instead of a trait):

Separate trait, to be re-used wherever you want:

trait testRelatedClass {
    function returnPrivatePropertyIfAllowed(object $caller, 
                                            string $propertyName,
                                            string $storedObjectPropertyName) {

        if ($caller->$storedObjectPropertyName === $this) 
              return $this->$propertyName;

        throw new \Exception("property $propertyName is only accessible to objects who have this instance of {$this::class} stored as a property $storedObjectPropertyName");
    }  
}

Which is a bit complicated, but efficient. This is how you use that trait:

class supplier {
    protected person $contactPerson;

    use testRelatedClass ;

    function contact(order $order): person {
        // use strong typing to prevent access from other classes 
        return $this->returnPrivatePropertyIfAllowed($order, 'contactPerson', 'supplier');
    }        
}

This hack wouldn't work:

class unrelated {
    public function unrelatedStuff(supplier $supplier){
        $this->supplier = $supplier;
        return $supplier->contact($this); // error because $this is not of class order
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.