Kirjautuminen

Haku

Tehtävät

Keskustelu: Koodit: PHP: Vahvan tyypityksen emulointi

tsuriga [06.01.2008 00:00:00]

#

Tyypitys tarkoittaa ohjelmoinnissa arvojen tietotyyppien käsittelyä. Heikossa tyypityksessä väärää tietotyyppiä olevat arvot yritetään automaattisesti muuttaa oikeaan tyyppiin, kun taas vahvassa tyypityksessä argumenteille ei suvaita vääriä tietotyyppejä.

PHP on lähtökohtaisesti heikosti tyypitetty kieli, vaikka versiosta 5.1 lähtien se tukeekin tyyppivihjailua (engl. type hinting), jonka avulla funktioiden parametreille voi asettaa vaadituksi tyypiksi joko taulukon tai tietyntyyppisen objektin. Jos on kuitenkin tottunut ohjelmoimaan vahvaa tyypitystä käyttäen, voi olla vaikeaa yrittää sopeutua tulkitsemaan ja korjaamaan virheitä heikkoa tyypitystä käyttävästä koodista. Vahvan tyypityksen yksi eduista heikkoon nähden kun on virhemarginaalin radikaali supistuminen, kun tulkki herjaa vääristä tietotyypeistä jo sijoittaessa.

Emuloinnin periaate

Luodaan säilöntäluokka ominaisuuksille, joka sisältää ominaisuuksien lisäksi myös ominaisuuksien odotetut tyypit. Ominaisuuksia lisätessä Säilöntäluokka sisältää metodit myös sekä määriteltyjen ominaisuuksien ja niiden arvojen, että määritettyjen ominaisuuksien odotettujen tyyppien merkkijonoesitysten palauttamiselle. Säilöntäluokka käyttää tyyppien tutkimiseen tyyppitutkija-apuluokkaa.

Koodin pitäisi olla jopa suhteellisen optimoitua PHP:ksi, vaikken suoritusnopeuksia testaillutkaan.

Huom! Esimerkki vaatii PHP version >=5.3.0, sillä PropertyValidator käyttää uutta ominaisuutta, jota sanotaan myöhäiseksi sidonnaksi (engl. Late static binding).

Properties.php

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

/**
 * Contains Properties class
 * for storing and validating
 * properties according to
 * set rules.
 *
 * PHP version 5
 */

/**
 * Property container that
 * uses customized rules
 * to validate properties.
 *
 * @author tsuriga
 * @version 1.1
 */
final class Properties
{

    /**
     * @var array Properties
     */
    private $_container;

    /**
     * @var array Rules for properties
     */
    private $_rules;

    /**
     * @var object Validator for properties
     */
    private $_validator;


    /**
     * Constructor. Takes an instance of an
     * object inheriting PropertyValidator
     * as its parameter.
     *
     * @param object A child class of PropertyValidator
     */
    public function __construct( PropertyValidator $validator )
    {
        $this->_container = array();
        $this->_rules = array();
        $this->_validator = $validator;
    }


    /**
     * Sets value to a property if the
     * passed value is of expected type.
     *
     * Attempts setting a value to a property,
     * and throws exception if no such
     * property is found, or passed value
     * does not conform to set rules.
     *
     * @param string The name of the property
     * @param mixed The value(s) of the property
     * @throws PropertyException If attempting to store wrong type of data to property, or if attempting to access nonexistent property
     */
    public function __set( $property, $value )
    {
        if ( isset( $this->_rules[ $property ] ) === true ):
            $expected = $this->_rules[ $property ];

            $accessGranted = ( is_array( $expected) === true ) ?
                             $this->_validator->validateMultiple(
                                                                  $property,
                                                                  $value,
                                                                  $expected
                                                                ):
                             $this->_validator->validateSingle(
                                                                $property,
                                                                $value,
                                                                $expected
                                                              );

            if ( $accessGranted === true ):
                $this->_container[ $property ] = $value;
            endif;
        else:
            throw new PropertyException( "No such property: '{$property}'" );
        endif;
    }


    /**
     * Returns requested property.
     *
     * @param string The name of property
     */
    public function __get( $property )
    {
        return $this->_container[ $property ];
    }


    /**
     * Returns all contained
     * properties.
     *
     * @return array Contained properties
     */
    public function getContents()
    {
        return $this->_container;
    }


    /**
     * Returns all properties'
     * rules as strings.
     *
     * @return array String representations of contained properties' rules
     */
    public function getRules()
    {
        $rules = array();

        foreach ( $this->_rules as $property => $rule ):
            $rules[ $property ] = $this->_validator->ruleToString( $rule );
        endforeach;

        return $rules;
    }


    /**
     * Registers a property and
     * assigns it its rules.
     *
     * @param string The name of the property
     * @param mixed The rule(s) of the property
     */
    public function register( $property, $ruleset )
    {
        $this->_rules[ $property ] = $ruleset;
    }

}

?>

PropertyValidator.class.php

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

/**
 * Contains abstract class PropertyValidator that
 * all property validators should inherit.
 */

/**
 * Base class for
 * property validators.
 *
 * @author tsuriga
 * @version 1.0
 */
abstract class PropertyValidator
{



    /**
     * Validates an arbitrary set of values.
     *
     * In addition to set of rules, accepts
     * also single rule that is then used
     * to validate all values.
     *
     * @param array|string Name or names of properties
     * @param array Values to validate
     * @param array|mixed Validation rule(s)
     * @return bool True if properties validate, false otherwise
     * @throws PropertyException If validation of any property fails.
     */
    public function validateSet( $properties, $values, $rules )
    {
        if ( is_array( $rules ) === false ):
            $rules = array_fill_keys( $properties, array_keys( $values ), $rule );
        endif;

        return static::validateMultiple( $properties, $values, $rules );
    }


    /**
     * Validates multiple values.
     *
     * Validates a set of values according
     * to submitted rules. When passed a
     * single rule, that one rule is used
     * to validate all the values.
     *
     * @param array|string Name or names of properties
     * @param array Values to validate
     * @param mixed Validation rules
     * @return bool True if properties validate, false otherwise
     * @throws PropertyException If validation of any property fails or the amounts of properties and rules did not match
     */
    public static function validateMultiple( $properties, $values, $rules )
    {
        /*
         * Ensure values and rules
         * have the same keys
         */
        if ( array_keys( $values ) === array_keys( $rules ) ):

            foreach( $values as $key => $value ):
                $rule = $rules[ $key ];

                /*
                 * Parse the name of the property
                 */
                $propertyName = ( is_array( $properties ) === true ) ?
                                $properties[ $key ]:
                                $properties . '[' . $key . ']';

                /*
                 * Return false if any of the
                 * values fails validation.
                 */
                if ( static::validateSingle(
                                             $propertyName,
                                             $value,
                                             $rule
                                           ) === false ):
                    return false;
                endif;

                next( $values );
            endforeach;

        /*
         * Throw exception and return false if the
         * keys of values and rules did not match.
         */
        else:
            throw new PropertyException( 'The keys of values did not match those of rules' );
            return false;
        endif;

        /*
         * No validation errors,
         * return true.
         */
        return true;
    }


    /**
     * Validates a single value.
     *
     * @param string Name of the property
     * @param mixed Value to validate
     * @param mixed Rule to use in validation process
     * @return bool True if value validates, false otherwise
     */
    abstract public static function validateSingle( $property, $value, $rule );


    /**
     * Return string
     * representation
     * for a rule.
     *
     * @param mixed Rule
     * @return string String representation of the rule
     */
    abstract public static function ruleToString( $rule );

}

?>

TypeValidator.class.php

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

/**
 * Contains typevalidator class for
 * enforcing types on named properties.
 *
 * PHP version 5
 */

/**
 * Class for enforcing
 * types on properties.
 *
 * CHANGELOG:
 *  1.1: Removed final declaration Modified class so that
 *       it now inherits the basic property validator.
 *
 * @author tsuriga
 * @version 1.1
 */
class TypeValidator extends PropertyValidator
{

    /**
     * @const int Type constants
     */
    const TYPE_INT      = 2;
    const TYPE_BOOL     = 3;
    const TYPE_FLOAT    = 4;
    const TYPE_STRING   = 5;
    const TYPE_ARRAY    = 6;
    const TYPE_RESOURCE = 7;

    /**
     * @var array String representations of types
     */
    protected static $_typeStrs = array(
                                         self::TYPE_INT      => 'integer',
                                         self::TYPE_BOOL     => 'boolean',
                                         self::TYPE_FLOAT    => 'float',
                                         self::TYPE_STRING   => 'string',
                                         self::TYPE_ARRAY    => 'array',
                                         self::TYPE_RESOURCE => 'resource',
                                       );


    /**
     * Tests the passed value
     * against the passed type.
     *
     * @param string Name of the property
     * @param mixed Value to validate
     * @param int|string Expected type. Either one of the type constants or the name of a class
     * @return bool True if the value is of expected type
     * @throws PropertyException If the property did not pass the validation
     */
    public static function validateSingle( $property, $value, $type )
    {
        $validates = true;

        /*
         * In case of an array, check if the
         * type of the array has also been
         * defined and separate it from
         * the array-type definition.
         */
        $arrayType;
        if ( (int)( (string)$type[ 0 ] ) === self::TYPE_ARRAY ):
            $arrayType = substr( (string)$type, 1 );
            if ( ctype_digit( $arrayType ) === true ):
                $arrayType = (int)$arrayType;
            endif;
            $type = self::TYPE_ARRAY;
        endif;

        switch( $type ):
            case self::TYPE_ARRAY:
                if ( is_array( $value ) === true ):
                    if ( $arrayType === null ):
                        $validates = true;
                    else:
                        foreach( $value as $k => $v ):
                            self::validateSingle(
                                                  "{$property}[{$k}]",
                                                  $v,
                                                  $arrayType
                                                );
                        endforeach;
                    endif;
                endif;
                break;
            case self::TYPE_BOOL:
                $validates = is_bool( $value );
                break;
            case self::TYPE_FLOAT:
                $validates = is_float( $value );
                break;
            case self::TYPE_INT:
                $validates = is_int( $value );
                break;
            case self::TYPE_STRING:
                $validates = is_string( $value );
                break;
            case self::TYPE_RESOURCE:
                $validates = is_resource( $value );
                break;
            default:
                $validates = ( $value instanceof $type );
        endswitch;

        /*
         * Throw exception if the value
         * of the property was not of
         * desired type.
         */
        if ( $validates === false ):

            /*
             * Get the string representations
             * of the passed and required types.
             */
            $givenType = self::parseType( $value );
            $expected = ( is_int( $type ) === true ) ?
                        self::$_typeStrs[ $type ] : $type;

            $msg = "Wrong type of data given for '%s' (expected %s, %s given)";
            $exceptionMsg = sprintf(
                                     $msg,
                                     $property,
                                     $expected,
                                     $givenType
                                   );

            throw new PropertyException( $exceptionMsg );

        /*
         * Value passed validation, return true.
         */
        else:
            return true;
        endif;
    }


    /**
     * Returns string representation(s) of type constant, or class name.
     *
     * @param int|string|array Type(s)
     * @return string|array String representation(s)
     */
    public static function ruleToString( $type )
    {
        // Type constant
        if ( is_int( $type ) === true ):
            return self::$_typeStrs[ $type ];

        // Array
        elseif ( is_array( $type ) === true ):
            $typeStrs = array();
            foreach( $type as $t ):
                $typeStrs[] = self::ruleToString( $t );
            endforeach;
            return $typeStrs;

        // Object
        else:
            return $type;
        endif;
    }


    /**
     * Chooses a method matching the arguments.
     *
     * @param string Method prefix
     * @param array Variations of a method
     * @param array Arguments
     * @return string|bool Name of the chosen method if found, false otherwise
     * @throws PropertyException If arguments did not match any of the variations
     */
    public static function chooseMethod( $origin, $variations, $args )
    {
        foreach ( $variations as $name => $types ):
            try {
                if ( self::validateSet( $name, $args, $types ) === true ):
                    return ( $origin . $name );
                endif;
            } catch ( PropertyException $e ) {
                continue;
            }
        endforeach;

        /*
         * Throw exception when calling a method
         * with incompatible parameters, ie.
         * when passed arguments didn't match
         * any variation.
         */
        $types = array_map( array( 'self', 'parseType' ), $args );
        $errorMsg = sprintf(
                             "%s called with incompatible arguments (%s)",
                             $origin,
                             implode( ', ', $types )
                           );
        throw new PropertyException( $errorMsg );

        return false;
    }


    /**
     * Returns the type of single value or
     * if $value is an array, the types of
     * values in the array.
     *
     * @param mixed Value to examine
     * @return string String representation of the value's type
     */
    public static function parseType( $value )
    {
        $str = '';

        /* Parse all types in an array */
        if ( is_array( $value ) === true ):
            $str = 'array(';
            $types = array();
            foreach( $value as $key => $v ):
                $types[] = $key . ' => ' . self::parseType( $v );
            endforeach;
            $str .= implode( ', ', $types ) . ')';

        /*
         * If value is an object, get
         * the class name. Additionally,
         * replace 'double' with 'float'.
         */
        else:
            $type = getType( $value );
            if ( $type === 'object' ):
                $type = get_class( $value );
            elseif ( $type === 'double' ):
                $type = 'float';
            endif;
            $str = $type;
        endif;

        return $str;
    }

}

?>

PropertyException.class.php

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

/**
 * Contains PropertyException
 *
 * PHP version 5
 */

/**
 * PropertyException
 *
 * @author tsuriga, 2007
 */
class PropertyException extends Exception
{

    /**
     * Constructor. Overridden so
     * that the message is no
     * longer optional.
     *
     * @param string Error message
     * @param int Error code
     */
    public function __construct( $message, $code = 0 )
    {
        parent::__construct( $message, $code );
    }

}

?>

type_example.php

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

/**
 * Brief examples of how to use
 * strict typing in PHP5.
 *
 * PHP version 5.
 */

/*
 * Required packages.
 */
require 'PropertyException.class.php';
require 'PropertyValidator.class.php';
require 'TypeValidator.class.php';
require 'Properties.class.php';

/*
 * Let's initialize new property
 * container that uses TypeValidator
 * to validate values.
 */
$p = new Properties( new TypeValidator() );

/*
 * Let's register some possible values.
 */
$p->register(
              'arrr',
              array(
                     'foo' => TypeValidator::TYPE_INT,
                     'bar' => TypeValidator::TYPE_STRING
                   )
            );
$p->register( 'post', TypeValidator::TYPE_ARRAY . TypeValidator::TYPE_INT );
$p->register( 'get', TypeValidator::TYPE_ARRAY );
$p->register( 'moo', TypeValidator::TYPE_INT );

/*
 * Let's set correct values at first.
 */
echo "\nAttempting to set correct values...\n";
try {
    $p->arrr = array( 'foo' => 20, 'bar' => 'baz' );
    $p->post = array( 20, 200 );
    $p->get = array( 'moo', 42 );
} catch ( PropertyException $e ) {
    die( $e->getMessage() );
}
echo "    No errors\n";


/*
 * Let's try setting an incorrect value.
 */
echo "\nAttempting to set incorrect value...\n";
try {
    /*
     * Single integer wanted, not an array of them.
     */
    $p->moo = array( 20, 80 );
} catch ( PropertyException $e ) {
    die( $e->getMessage() . "\n" );
}

?>

map_ [08.01.2008 00:28:39]

#

Tällaiseen systeemiin voisi saman tien lisätä muitakin rajoituksia tietotyypin lisäksi. Merkkijonomuuttujalle voisi antaa vaikka säännöllisen lausekkeen, jonka on tunnistettava annettu arvo, taikka minimi- ja maksimipituuden. Numeerisille muuttujille taas voisi antaa minimi- ja maksimiarvon jne.

Pian voidaankin toteuttaa melko vaivattomasti yksinkertainen mutta helppokäyttöinen lomakkeiden tarkistaja:

<?php
try {
    foreach ($_POST as $nimi => $arvo)
        $properties->$nimi = $arvo;
} catch (TypeMismatchException $e) {
    /* Käyttäjälle virheilmoitus...
     * Poikkeukseen voinee liittää kaikenlaista tietoa,
     * jolla saadaan aikaan käyttäjäystävällisempi
     * virheilmoitus.
     */
}

tsuriga [08.01.2008 11:03:21]

#

Totta. Tätä en kyllä tullut ajatelleeksi kun julistin PropertyExaminerin finaliksi, senhän voisi periä ja ylikirjoittaa tuon validate-metodin. Silloin Properties-luokalle pitäisi antaa PropertyExaminerista olio.. Pitäneepä kehitellä koodia eteenpäin, tattis ideasta.

Ja jotta koodissa olisi helpompi viitata niin muuttujaahan voi aina lyhentää vaikka muotoon $prop tai jopa $p. Tai yllämainitussa tapauksessa jos halutaan kirjoittaa itsedokumentoituvaa koodia niin muuttujaa voisi kutsua vaikkapa nimellä $formValues tms.

EDIT: PHP:ssä on näköjään myös Filter-extensioni, pitääpä tutkia.

23.01.2008: Päivitin esimerkkiä rankalla kädellä. Nyt voi periä PropertyValidatorin ja kirjoittaa oman validaattorin. Koska tässä oli nyt kyse tyypityksestä niin tein TypeValidatorin. Properties-luokalle annetaan sitten parametrina haluttu validaattori. Vaatii muutosten myötä myös tällä hetkellä vielä kehitysvaiheessa olevan version 5.3.0 tai uudemman. Filterinkin voi toki wrapata jos tahtoo, en sitten tiedä onko ehkä nopeampaa ja ehkä jopa selkeämpää kuitenkin käyttää sitä suoraan?

Vastaus

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

Tietoa sivustosta