2021/11/24/things you can't do in PHP

From Woozle Writes Code
Jump to navigation Jump to search
Codeblog

Take 1: my bad

This is where I started -- and admittedly there was already a problem in that I'm useing taScalarAccess twice in cScalarReadOnly (once each via cScalarLocal and taReadOnlyScalar). The idea, though, was to override the SetIt() and ClearIt() methods in whatever class was using taReadOnlyScalar. Those methods being implemented in cScalarLocal, the use of the taReadOnlyScalar trait was intended to rename those methods with a "_" prefix and call them conditionally from the replacement methods.

trait taScalarAccess {
    abstract public function SetIt(mixed $v);
    abstract public function GetIt() : mixed;
    abstract public function HasIt() : bool;
    abstract public function ClearIt();
}
trait tScalarLocal {
    use taScalarAccess;
    public function GetIt() : mixed {}
    public function HasIt() : bool {}
    public function SetIt(mixed $v) {}
    public function ClearIt() {}
}
trait taReadOnlyScalar {
    use taScalarAccess {
      taScalarAccess::SetIt as protected _SetIt;
      taScalarAccess::ClearIt as protected _ClearIt;
      }
    public function SetIt(mixed $v) { if ($something) { $this->_SetIt($v); } }
    public function ClearIt() { if ($something) { $this->_ClearIt(); } }
}

class cScalarLocal {
    use taScalarAccess;
    use tScalarLocal;
}
class cScalarReadOnly extends cScalarLocal {
    use tScalarLocal;
    use taReadOnlyScalar;
}

Result

PHP Fatal error: Trait method SetIt has not been applied, because there are collisions with other trait methods on cScalarReadOnly in /home/htnet/site/git/ferreteria/base/tests/php2.php on line 28

Take 2: decouple the conflicting traits

First, I tried moving use taScalarAccess out of tScalarLocal, even though I wanted the latter to descend from the former because of a method implemented in the former (not shown). Classes that use tScalarLocal would also need to explicitly use taScalarAccess in order to get that functionality.

trait taScalarAccess {
    abstract public function HasIt() : bool;
    abstract public function GetIt() : mixed;
    abstract public function SetIt(mixed $v);
    abstract public function ClearIt();
}
trait tScalarLocal {
    public function HasIt() : bool {}
    public function GetIt() : mixed {}
    public function SetIt(mixed $v) {}
    public function ClearIt() {}
}
trait taReadOnlyScalar {
    use taScalarAccess {
      taScalarAccess::SetIt as protected _SetIt;
      taScalarAccess::ClearIt as protected _ClearIt;
      }
    public function SetIt(mixed $v) { if ($something) { $this->_SetIt($v); } }
    public function ClearIt() { if ($something) { $this->_ClearIt(); } }
}

class cScalarLocal {
    use taScalarAccess;
    use tScalarLocal;
}
class cScalarReadOnly extends cScalarLocal {
    use tScalarLocal;
    use taReadOnlyScalar;
}

Result

PHP Fatal error: Trait method SetIt has not been applied, because there are collisions with other trait methods on cScalarReadOnly in /home/htnet/site/git/ferreteria/base/tests/php2.php on line 27

i.e. that didn't fix the problem.

Take 3: interface

Next, I tried moving the abstract functions from taScalarAccess into an interface, to avoid the collision. (Had this worked, it would have been nice if traits could implement interfaces, but it didn't so the point is moot as far as this issue goes.)

interface ifScalarAccess {
    function HasIt() : bool;
    function GetIt() : mixed;
    function SetIt(mixed $v);
    function ClearIt();
}
trait taScalarAccess {}
trait tScalarLocal {
    public function HasIt() : bool {}
    public function GetIt() : mixed {}
    public function SetIt(mixed $v) {}
    public function ClearIt() {}
}
trait taReadOnlyScalar {
    use taScalarAccess {
      taScalarAccess::SetIt as protected _SetIt;
      taScalarAccess::ClearIt as protected _ClearIt;
      } //*/
    public function SetIt(mixed $v) { if ($something) { $this->_SetIt($v); } }
    public function ClearIt() { if ($something) { $this->_ClearIt(); } }
}

class cScalarLocal implements ifScalarAccess {
    use taScalarAccess;
    use tScalarLocal;
}
class cScalarReadOnly extends cScalarLocal {
    use tScalarLocal;
    use taReadOnlyScalar;
}

Result

PHP Fatal error: An alias was defined for taScalarAccess::SetIt but this method does not exist in /home/htnet/site/git/ferreteria/base/tests/php2.php on line 15

Take 4: hide the real write fx()

Here's what I ended up with the next day, including bits of code excluded in the examples above. I basically started with the _SetIt() and _ClearIt() functions being defined even where we're just going to call them directly from SetIt() and ClearIt(), which makes it easy to just override the latter two so they can act conditionally where needed.

abstract class caScalarAccess {
    // DIRECT: WRITE
    abstract protected function _SetIt(mixed $v);
    abstract protected function _ClearIt();
  
    // API: WRITE (default)
    public function SetIt(mixed $v) { return $this->_SetIt($v); }
    public function ClearIt() { $this->_ClearIt(); }
    // API: READ
    abstract public function HasIt() : bool;
    abstract public function GetIt() : mixed;
    public function GetItNz($default=NULL) { return $this->HasIt() ? $this->GetIt() : $default; }
    
    // IDE
    public function DumpLine() : string {
        $bHas = $this->HasIt();
        $sVal = $bHas ? (values\csFormats::Render($this->GetIt())) : cEnv::ItalIt('(not set)');
        $ftClass = cEnv::BoldIt(get_class($this));
        $out = "Piece class: $ftClass Value: [$sVal]";
        return $out;
    }
}
// PURPOSE: value is stored internally
trait tScalarLocal {
    private $tscl_value;
    private $tscl_exists = FALSE;
  
    public function HasIt() : bool { return $this->tscl_exists; }
    public function GetIt() : mixed { return $this->tscl_value; }
    protected function _SetIt(mixed $v) {
        $this->tscl_value = $v;
        $this->tscl_exists = TRUE;
    }
    protected function _ClearIt() {
        $this->tscl_exists = FALSE;
        #unset($this->value);
    }
}
/*::::
  REQUIRES: tScalarLocal
  * We can't just "use" that here, though, because cScalarReadOnly ends up using both of them.
*/
trait tScalarReadOnly {
  
    private $canWrite = FALSE;
    
    protected function SetCanWrite(bool $b) { $this->canWrite = $b; }
    protected function GetCanWrite() : bool { return $this->canWrite; }

    // ++ API ++ //
    
    public function SetIt(mixed $v) {
        if ($this->GetCanWrite()) {
            $this->_SetIt($v);
        } else {
            $this->ThrowWriteError('set');
        }
    }
    public function ClearIt() {
        if ($this->GetCanWrite()) {
            $this->_ClearIt();
        } else {
            $this->ThrowWriteError('clear');
        }
    }
    
    // -- API -- //
    // ++ ERROR ++ //
    
    protected function ThrowWriteError(string $sAction) {
        $sClass = get_class($this);
        $sError = "trying to $sAction the value of a read-only $sClass object.";
        $e = new except\cUsage($sError);
        throw $e;
    }
    
    // -- ERROR -- //
}

class cScalarLocal { use tScalarLocal; }
/*::::
  THINKING: This is neither writeable nor a reference, so local storage should always be sufficient.
*/
class cScalarReadOnly extends cScalarLocal {
    use tScalarLocal;
    use tScalarReadOnly;
    
    public function __construct(mixed $value=NULL, bool $exists=FALSE) {
        $this->SetWritable(FALSE);
        if ($exists) {
            $this->_SetIt($value);
        } else {
            $this->_ClearIt();
        }
    }
}