Kirjautuminen

Haku

Tehtävät

Keskustelu: Koodit: PHP: Moniperinnän emulointi

tsuriga [15.01.2008 00:00:00]

#

Mitä ihmettä?

Joku jossakin kehtasi väittää, että PHP:stä ei löydy tukea moniperinnälle. Ja oikeassahan hän oli, joten siitä se sitten lähti. Kyseessä on siis todellakin vain huvin ja urheilun vuoksi suoritettu kokeilu, jossa tutkittiin mahdollisuuksia moniperinnän emulointiin PHP-kielellä – varsinaista käyttöä varten suosittelen odottamaan kieleltä natiivia tukea moniperintään (ei tiettävästi tekeillä?). Suoritusnopeuksia en kehdannut edes mittailla. Kommentit toimintaperiaatteen oikeellisuudesta otetaan lämmöllä vastaan.

Periaate

Muodostetaan isäntäluokista maagiset lapsiluokat, jotka perivät kukin aina yhden isännän. Muodostetaan lisäksi yksi välittäjäluokka, joka hoitaa luokkien jäsenten väliset interaktiot. Peritään välittäjäluokka.

Käytännössä välittäjäluokassa määritellään uusiksi isäntäluokkien tarjoamat metodit, jotka sitten kutsuvat isäntäluokkien alkuperäismetodeja. Luokkaominaisuuksien tallennuksessa välittäjä- ja isäntäluokkiin käytetään esimerkissä pollausta, mikä tekee toiminnasta purkan oloista. Samanlaista lähestymistapaa käyttävät myös staattiset jäsenet. Staattisuuden kohdalla joudutaan tinkimään aitoudesta sen verran, että staattiset jäsenet tulee asettaa setStaticProperty-metodin kautta sen sijaan, että niihin voisi suoraan tallentaa komentamalla Luokka::$staattinen = 2;.

Luokkien jäsenet voidaan selvittää joko koodista, tai sitten käyttämällä PHP:n tarjoamaa Reflection APIa.

Toinen lähestysmistapa voisi olla käyttää PHP:n runkit-lisäosaa, mutta en ainakaan itse saanut sitä toimimaan, vaan aina metodia kopioidessa CLI kaatui.

Sanastoa

Otetaan nyt mukaan vielä vähän sanastoa kun en muista näitä ilmaisuja juuri käytetyn vanhemmissa vinkeissä.

engl.suom.Selitys
memberjäsenluokka, muuttuja, vakio tai metodi
reflectionheijastusjäsenen kuvaus / ilmentymä
propertyominaisuusluokkamuuttuja
methodmetodiluokkafunktio

Huomioita

Vaatii PHP:stä vähintään version 5.3.x tai suuremman. Listattuna on siis kaikki koodi, jonka toiminnallisuuden emulointi vaatii. Kunhan saan valmiiksi noita generaattoreita, jolla näitä maagisia luokkia saa luotua automaattisesti, niin silloin riittää seuraavat muutama komentoa, eikä tähän käsipelinysväykseen ole siis tarvetta:

Komentorivi:
    php create-magic-classes A+B

PHP-koodissa:
    require 'AB_Magic.class.php';

class MultipleParents extends AB_Magic ...

Isäntäluokat

<?php

/**
 * Class 1 to inherit.
 */
class A
{

    /**
     * @var mixed Public property that is shared between classes A and B in the example
     */
    public $foo = 'A';

    /**
     * @const int Example of constant inheritance. Children inherit constants as well
     */
    const EXAMPLECONSTANT = 1;


    /**
     * An example of constructor
     * with some miscellaneous
     * parameters. Class B will
     * have none.
     */
    public function __construct( $x, $y ) { }


    /**
     * A method that will never
     * be called in our multiple
     * inheritance example because
     * class B's test method
     * succeeds this.
     *
     * @param mixed Public $property foo's new value
     * @return string Information about this method being called
     */
    public function test()
    {
        return 'A->test called!';
    }


    /**
     * An example of a protected method.
     * Sets public property $foo to whatever
     * is submitted to the metho as parameter.
     *
     * @param mixed Public property $foo's forthcoming value
     * @return string Information about this method being called
     */
    protected function testProtected( $foo )
    {
        $this->foo = $foo;
        return 'A::testProtected() functional!';
    }

}


/**
 * Class 2 to inherit.
 */
class B
{

    /*
     * Variables from different
     * scopes for testing purposes.
     */
    public $foo = 'B';
    protected $_baz = 'baz';
    private $_priv = 'private';

    /**
     * @var string Example of static string
     */
    public static $static_string = 'Initial B';


    /**
     * This will be called from AB_Magic
     * instead of A's test()-method. Sets
     * public property $foo to 'B'.
     *
     * @return string Either arguments separated with newlines or information about this method being called
     */
    public function test()
    {
        $this->foo = 'B';

        $args = func_get_args();
        if ( count( $args ) > 0 ) {
            return implode( "\n        ", $args);
        }
        else {
            return 'B->test called';
        }
    }


    /**
     * Static method example.
     * Modifies the public static
     * property $static_string to
     * indicate access to this method.
     */
    public static function testStatic()
    {
        self::$static_string = 'Secondary B';
        echo 'B::testStatic() called!';
    }

}

?>

Maagiset luokat

<?php

/**
 * Child class for handling
 * parent's members. Without
 * these we wouldn't be able
 * to access protected
 * members.
 */
final class A_Magic extends A
{

    /**
     * Overloads getter in
     * order to be able to
     * read also protected
     * properties.
     *
     * @var string Property to read
     */
    public function __get( $var )
    {
        return $this->$var;
    }


    /**
     * Overloads setter in
     * order to be able to
     * modify also protected
     * properties.
     *
     * @param string Property to modify
     * @param mixed Value to set
     */
    public function __set( $var, $value )
    {
        $this->$var = $value;
    }


    /**
     * Wrapper for parent class'
     * protected method testProtected().
     * Note that the scope here is public
     * so that it can be called from
     * our main magic class.
     */
    public function testProtected( $foo )
    {
        /*
         * See host class' comments
         * for testProtected-method.
         */
        return parent::testProtected( $foo );
    }

}


/**
 * Child class for handling
 * parent's members.
 *
 * No need to wrap static
 * methods since we call
 * them directly from parent
 * class.
 */
final class B_Magic extends B
{

    public function __get( $var )
    {
        return $this->$var;
    }

    public function __set( $var, $value )
    {
        $this->$var = $value;
    }

}


/**
 * Magic class that acts as a proxy
 * between child class and its
 * parents.
 */
class AB_Magic
{

    /*
     * Used to determine
     * whether access to
     * particular property
     * is permitted.
     */
    const SCOPE_PUBLIC = 1;
    const SCOPE_PROTECTED = 2;

    /*
     * Define all parents' constant here
     */
    const EXAMPLECONSTANT = 1;

    /**
     * @var array Names of parents
     *
     * Can be used for shortening code.
     * Omit if not required.
     * (used only once in this
     * example if at all IIRC)
     */
    private static $_parents = array( 'A', 'B' );

    /**
     * @var array Parents' static properties
     *
     * 'staticproperty' => array(classes)
     */
    private static $_staticProperties = array(
                                               'static_string' =>
                                                   array(
                                                          'B'
                                                        )
                                             );

    /*
     * We need to define any static properties
     * in the main magic class in order to make
     * them accessible to children.
     */
    public static $static_string = 'Initial B';

    /**
     * @var array Objects from parent classes
     */
    private $_objects;

    /**
     * @var array References to parent objects and scopes for each property
     */
    private $_properties;

    /**
     * @var array Names of parents' mutual public and protected properties
     *
     * This can be omitted if there are no mutual properties.
     */
    private $_mutualProperties;


    /**
     * Constructor.
     *
     * @param mixed Parameters for parent classes
     */
    public function __construct( $x, $y )
    {
        /*
         * First get arguments given
         * to this constructor for
         * passing them forward.
         * Omit if parent classes
         * don't take arguments.
         */
        $args = func_get_args();

        /*
         * Create instances from parent classes.
         */
        $this->_objects = array();
        $this->_objects[ 'A' ] = new A_Magic( $x, $y );
        $this->_objects[ 'B' ] = new B_Magic();

        /*
         * Attach each and every property
         * to their respective parent
         * instances. Note that properties
         * may have multiple parents.
         */
        $this->_properties = array();

        /*
         * For each property there
         * are three indexes:
         *  scope:   scope constant
         *  objects: references to parents
         *  value:   current value
         *
         * Remember that properties of same
         * name must have also same scopes.
         */
        // property: foo
        $this->_properties[ 'foo' ] = array();
        $this->_properties[ 'foo' ][ 'scope' ] =
            self::SCOPE_PUBLIC;
        $this->_properties[ 'foo' ][ 'objects' ] =
            array(
                   &$this->_objects[ 'A' ],
                   &$this->_objects[ 'B' ]
                 );
        $this->_properties[ 'foo' ][ 'value' ] =
            $this->_objects[ 'B' ]->foo;
        // property: baz
        $this->_properties[ 'baz' ][ 'scope' ] =
            self::SCOPE_PROTECTED;
        $this->_properties[ 'baz' ][ 'objects' ] =
            array( &$this->_objects[ 'B' ] );
        $this->_properties[ 'baz' ][ 'value' ] =
            $this->_objects[ 'B' ]->baz;

        /*
         * List mutual properties.
         */
        $this->_mutualProperties = array();
        $this->_mutualProperties[ 'foo' ] = array( 'A', 'B' );
    } // constructor


    /**
     * Overload getter to fetch
     * properties from a parent
     * object.
     *
     * @param string Property to fetch
     * @return Property from a parent object, or null if not found or accessible
     * @throws E_USER_NOTICE If property is not found or attempting direct access to protected property
     */
    public function __get( $var )
    {
        if ( isset( $this->_properties[ $var ] ) === true ) {

            /**
             * Dirty hack for determining
             * the origin of the property
             * call.
             */
            $debug = debug_backtrace();
            $callScope = ( count( $debug ) < 2 ) ?
                         self::SCOPE_PUBLIC:
                         self::SCOPE_PROTECTED;

            /*
             * Return property only if the call
             * has privileges to access it.
             */
            if ( $callScope >= $this->_properties[ $var ][ 'scope' ] ) {
                return $this->_properties[ $var ][ 'value' ];
            }

            /*
             * Otherwise trigger E_USER_NOTICE
             * for trying to access protected
             * property outside scope.
             */
            else {
                /*
                 * We parse child class' name
                 * from debug backtrace. A neat
                 * hack for emulating PHP's real
                 * error message when trying to
                 * access a private or non-
                 * existent property.
                 */
                $debug = debug_backtrace();
                $childClass = get_class( $debug[ 0 ][ 'object' ] );

                /*
                 * Note that the line PHP reports
                 * in the error message is the line
                 * where trigger_error is called.
                 */
                $errorMsg = 'Cannot access protected property ' .
                            $childClass . '::$' . $var;
                trigger_error( $errorMsg, E_USER_ERROR );

                return null;
            }

        }

        /*
         * Property was not found,
         * trigger E_USER_NOTICE.
         */
        else {
            $debug = debug_backtrace();
            $childClass = get_class( $debug[ 0 ][ 'object' ] );

            $errorMsg = 'Undefined property: ' . $childClass . '::$' . $var;
            trigger_error( $errorMsg, E_USER_NOTICE );

            return null;
        }
    }


    /**
     * Overload setter to store
     * properties within
     * parent objects.
     *
     * @param string Property to store the value in
     * @param mixed Value to store
     */
    public function __set( $name, $value )
    {
        foreach( $this->_properties[ $name ][ 'objects' ]as $object ) {
            $object->$var = $value;
        }
        $this->_properties[ $name ][ 'value' ] = $value;
    }


    /**
     * Updates all static properties.
     * If parents don't have any static
     * properties, this method should be
     * omitted.
     *
     * @param string Name of the parent class where properties are fetched from
     */
    private static function _updateStaticProperties( $parent )
    {
        /*
         * Loop through the static properties and
         * update own and every parent's properties.
         */
        foreach ( self::$_staticProperties as $property => $classes ) {
            self::$$property = $parent::$$property;
            $parentKey = array_search( $parent, $classes, true );
            if ( $parentKey !== false ) {
                unset( $classes[ $parentKey ] );
                foreach ( $classes as $class ) {
                    $class::$$property = $parent::$$property;
                }
            }
        }
    }


    /**
     * Hack method for modifying class' static properties.
     * Better suggestions requested.
     *
     * @param string Name of the static property
     * @param mixed Value of the static property
     * @throws E_USER_ERROR If static property is not found
     */
    public static function setStaticProperty( $property, $value )
    {
        $propertyIsSet = false;

        /*
         * See if this class has a
         * property called $property
         * and update the property's
         * contents if it does.
         */
        if ( isset( self::$$property ) === true ) {
            self::$$property = $value;
            $propertyIsSet = true;
        }

        if ( isset( self::$_staticProperties[ $property ] ) === true ) {
            $propertyIsSet = true;
            foreach ( self::$_staticProperties[ $property ] as $class ) {
                $class::$$property = $value;
            }
        }

        /*
         * If no such property was found,
         * trigger fatal error. Notice that
         * here we can't get the child class'
         * name from the debug queue so we'll
         * use just the property's name in the
         * error message. We could always
         * hardcode the child class' name :).
         */
        if ( $propertyIsSet === false ) {
            $errorMsg = 'Access to undeclared static property: $' . $property;
            trigger_error( $errorMsg, E_USER_ERROR );
        }
    }


    /**
     * Updates all the parents' properties.
     * If parents don't have mutual public
     * properties, this method only slows
     * down execution and should be omitted.
     *
     * @param object Reference to a parent object to read properties from
     */
    private function _updatePropertyReferences( $parent )
    {
        $parentClass = get_parent_class( $parent );
        /*
         * Loop through the mutual properties of parents
         * and update the most recent value from the
         * passed parent.
         *
         * We don't even bother checking whether
         * the index exists because no method that
         * does not modify any mutual properties
         * should be calling this method.
         */
        foreach ( $this->_mutualProperties as $name => $classes ) {
            if ( in_array( $parentClass, $classes, true ) === true ) {
                foreach ( $classes as $class ) {
                    $this->_objects[ $class ]->$name = $parent->$name;
                }
                $this->_properties[ $name ] = $parent->$name;
            }
        }
    }


# HERE GO ALL THE PUBLIC AND PROTECTED
# METHODS FROM PARENT CLASSES AS DEPICTED BELOW


    /**
     * We do need to define the
     * static method here so that
     * it'd be accessible from the
     * child class that inherits
     * our magic class.
     */
    public static function testStatic()
    {
        /*
         * For static methods we
         * make direct calls to
         * the parent classes.
         */
        B::testStatic();

        /*
         * Every static method that
         * modifies a static property
         * in the parent class' scope
         * should have the following
         * call.
         */
        self::_updateStaticProperties( 'B' );
    }


    /**
     * Magic test function that returns
     * parent class' test-method's return
     * value. In this case B's test-method
     * is called since B comes later in the
     * inheritance tree and thus succeeds
     * A's test-method.
     *
     * @param mixed Parent's test-method's parameters
     * @return mixed Whatever parent::test($args) returns
     */
    public function test()
    {
        /*
         * Since we don't always know the parameters
         * beforehand, we can use a dynamical approach
         * for calling the parent class' method.
         */
        $args = func_get_args();
        $val = call_user_func_array(
                                     array(
                                            $this->_objects[ 'B' ],
                                            'test'
                                          ),
                                     $args
                                   );

        /*
         * Update property references in case this
         * parent class' method modified some
         * properties
         */
        $this->_updatePropertyReferences( $this->_objects[ 'B' ] );
        return $val;
    }


    /**
     * Magic testProtected method that returns
     * A-class' testProtected-method's return
     * value since class B does not redefine
     * the method. Note the scope here is
     * "protected" so that it's only
     * accessible from inside the
     * class tree.
     *
     * @param mixed Input for testProtected
     * @return mixed Whatever parent class' testProtected returns
     */
    protected function testProtected( $foo )
    {
        /*
         * Since we know the parameters,
         * we can pass them directly
         */
        $val = $this->_objects[ 'A' ]->testProtected( $foo );

        $this->_updatePropertyReferences( $this->_objects[ 'A' ] );
        return $val;
    }

}

?>

Pääluokka ja testaukset

<?php
/* encoding=utf-8 */

/**
 * Brief, commented tutorial on how to loosely
 * emulate multiple inheritance in PHP.
 *
 * If one has no access to view the parent
 * parent classes' code, PHP's Reflection API
 * can be used for examining properties, methods
 * and parameters.
 *
 * Another way to accomplish similar behaviour
 * would be to use PHP's runkit extension but
 * unfortunately even the latest 0.9 version
 * of the DLL seemed unstable and crashed my CLI.
 *
 * Example is intended to serve only as a fun
 * experiment and any actual use of the presented
 * techniques is strongly discouraged. I have no
 * idea how this performs but I would imagine it's
 * nothing too splendid already due to redundant
 * property updates.
 *
 * Required PHP version >=5.3.x
 */


/**
 * Class for testing multiple inheritance.
 */
class MultipleParents extends AB_Magic
{

    /**
     * @var string An example of a private property
     */
    private $_var = 'private';


    /**
     * Wrapper for protected
     * testProtected-method.
     */
    public function runTestProtected( $foo )
    {
        return $this->testProtected( $foo );
    }

    /**
     * An example demonstrating how
     * private properties work in
     * the main class.
     *
     * @return string Private property $_var
     */
    public function getVar()
    {
        return $this->_var;
    }

}


# TESTING


/*
 * Let's run some tests
 */

echo "\nTesting static members:\n";
echo '    Accessing static property $static_string:', "\n";
echo '        ', MultipleParents::$static_string, "\n";
echo '    Accessing static method testStatic():', "\n";
echo '        ', MultipleParents::testStatic(), "\n";

/*
 * Let's see if we can access our static property
 * and whether it has changed as it should've.
 */
echo '        ', MultipleParents::$static_string;
echo "\n";
/*
 * Let's change the static
 * property directly.
 *
 * Notice the fugly hack since
 * I couldn't think of any other
 * way to make also the parent
 * classes' static properties
 * change simultaneously.
 */
MultipleParents::setStaticProperty(
                                    'static_string',
                                    'Final static B!'
                                  );
echo '    Reading $static_string after the change:', "\n";
echo '        ', MultipleParents::$static_string, "\n";
/*
 * The following would generate fatal error
 * because we're trying to access a static
 * property that doesn't exist.
 *
MultipleParents::setStaticProperty( 'null', 'Static property does not exist!' );
 */

/*
 * Notice the parameters for class A
 */
$multi = new MultipleParents( 100, 100 );

echo "\nTesting arbitrary parameter passing:\n";
$retval = $multi->test( 'foo', 'bar' );
echo "    test('foo', 'bar') returned:\n        ", $retval, "\n";

/*
 * Calling protected method outside class results in fatal error.
 * Remove comments should you wish to test this.
 *
echo 'Attempting to access protected method directly: ', "\n";
echo '    ', $multi->runTestProtected(), "\n";
 */
/*
 * Instead, we can call the wrapper above as follows:
 * (notice the parameter for setting $multi object's
 * public property $foo to 'A')
 */
echo "\nAttempting to access protected method via wrapper method:\n";
echo '    ', $multi->runTestProtected( 'A' ), "\n";

echo "\nTesting properties:\n";

echo "\n    Testing inheritance of constants:\n";
echo '        ', MultipleParents::EXAMPLECONSTANT, "\n";

/*
 * Parent class A set $foo to 'A' when
 * we called its testProtected-method.
 */
echo '    Getting $foo after calling A\'s testProtected():', "\n";
echo '        ', $multi->foo;
/*
 * Now parent class B's test-method will set $foo back to 'B'
 */
$multi->test();
echo "\n", '    Getting $foo after calling B\'s test():', "\n";
echo '        ', $multi->foo, "\n";

/*
 * Following would throw fatal error and halt
 * execution. Uncomment if you wish to test this.
 *
echo "    Attempting direct access to protected property: \n";
echo '        ', $multi->_baz, "\n\n";
*/

echo "    Attempting direct access to private property:\n";
/*
 * Throws notice and returns null because MultipleParents
 * class does not have have access to parents'
 * private properties.
 */
echo '        ', $multi->_priv, "\n";

/*
 * Works as expected if anyone else
 * besides me was wondering.
 */
echo "\n", "    Attempting to read a private property from\n    the main class through a public wrapper:\n";
echo '        ', $multi->getVar(), "\n";

?>

Kray [15.01.2008 17:07:03]

#

Kaikkeen se php pystyy...

tsuriga [15.01.2008 18:57:29]

#

Venyy ja paukkuu :). Kauhea purkkaviritelmähän tämä on, ihan mielenkiinnosta vaan lähdin kokeilemaan, että olisko mahdollista käyttää useamman luokan metodeita jotenkin yhdessä luokassa.. En oikeasti tiedä moniperinnästä paljoakaan, joten esim. ominaisuuksien periytyminen voi mennä esimerkissä ihan mehtään. Nyt kaikki metodin isäntäolion protected ja public ominaisuudet kirjoitetaan kaikille oliolle uusiks aina mitä tahansa metodia kutsuessa (paitsi jos koodi on tunnettua kuten esimerkissä).

qeijo [23.08.2013 20:52:55]

#

Onneksi nykyään on traits.

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta