2
\$\begingroup\$

I'm writing a package that is designed to provide a convenient way to inspect the schema for a database. The goal is that I can use it for generator code to suit the schema. The complete code can be found at https://github.com/courtney-miles/schnoop.

For example, I have classes for IntType, VarCharType, BlobType, etc. And I need to be able to construct these types from the results of the query, SHOW FULL COLUMNS FROM tbl;

These data type objects conform to a common interface, DataTypeInterface.

<?php

interface DataTypeInterface
{
    /**
     * Get the name of the type.
     * @return string
     */
    public function getName();

    /**
     * Indicates if a default value can be used with this type in a column.
     * @return bool
     */
    public function allowDefault();

    /**
     * Cast a value from MySQL to a suitable PHP type.
     * @param mixed $value
     * @return mixed
     */
    public function cast($value);
}

Additional interfaces are combined to reflect the unique properties of the data types. For example, this is the interface for used by all integer types.

<?php

interface IntTypeInterface extends DataTypeInterface
{
    public function getDisplayWidth();
    public function isSigned();
    public function getMinRange();
    public function getMaxRange();
}

(To be more correct, the IntTypeInterface actually extends other interfaces that specify these properties because there is an overlap with Floating Point Types. If you are interested in precisely all the interfaces involved, refer to https://github.com/courtney-miles/schnoop/blob/master/src/Schema/MySQL/DataType/IntTypeInterface.php)

So this is my current factory method which is obviously quite ugly.

<?php

class Factory
{
    // ...

    /**
     * @param string $dataTypeString Data type string as reported by MySQL when executing SHOW FULL COLUMNS FROM table;
     * @param null $collation
     */
    public function createDataType($dataTypeString, $collation = null)
    {
        $dataType = null;

        if (preg_match('/^(tiny|small|medium|big)?int(eger)?\((\d+)\)( unsigned)?/i', $dataTypeString, $matches)) {
            $displayWidth = $matches[3];
            $signed = empty($matches[4]);

            switch (strtolower($matches[1])) {
                case 'tiny':
                    $dataType = new TinyIntType($displayWidth, $signed);
                    break;
                case 'small':
                    $dataType = new SmallIntType($displayWidth, $signed);
                    break;
                case 'medium':
                    $dataType = new MediumIntType($displayWidth, $signed);
                    break;
                case 'big':
                    $dataType = new BigIntType($displayWidth, $signed);
                    break;
                default:
                    $dataType = new IntType($displayWidth, $signed);
                    break;
            }
        } elseif (preg_match('/^(decimal|float|double)\((\d+),(\d+)\)( unsigned)?/i', $dataTypeString, $matches)) {
            $precision = $matches[2];
            $scale = $matches[3];
            $signed = empty($matches[4]);

            switch (strtolower($matches[1])) {
                case 'decimal':
                    $dataType = new DecimalType($precision, $scale, $signed);
                    break;
                case 'double':
                    $dataType = new DoubleType($precision, $scale, $signed);
                    break;
                case 'float':
                    $dataType = new FloatType($precision, $scale, $signed);
            }
        } elseif (preg_match('/^(var)?binary\((\d+)\)$/i', $dataTypeString, $matches)) {
            $isVarBinary = !empty($matches[1]);
            $length = $matches[2];

            if (strtolower($isVarBinary)) {
                $dataType = new VarBinaryType($length);
            } else {
                $dataType = new BinaryType($length);
            }
        } elseif (preg_match('/^bit\((\d+)\)$/i', $dataTypeString, $matches)) {
            $dataType = new BitType($matches[1]);
        } else {
            $characterSet = !empty($collation) ? $this->getCharacterSet($collation) : null;

            if (preg_match('/^(var)?char\((\d+)\)$/i', $dataTypeString, $matches)) {
                $isVarChar = !empty($matches[1]);
                $length = $matches[2];

                if ($isVarChar) {
                    $dataType = new VarCharType($length, $characterSet, $collation);
                } else {
                    $dataType = new CharType($length, $characterSet, $collation);
                }
            } elseif (preg_match("/(set|enum)(\('.+'\))/i", $dataTypeString, $matches)) {
                $options = $this->parseOptions($matches[2]);

                switch (strtolower($matches[1])) {
                    case 'set':
                        $dataType = new SetType($options, $characterSet, $collation);
                        break;
                    case 'enum':
                        $dataType = new EnumType($options, $characterSet, $collation);
                        break;
                }
            } else {
                switch (strtolower($dataTypeString)) {
                    case 'text':
                        $dataType = new TextType($characterSet, $collation);
                        break;
                    case 'tinytext':
                        $dataType = new TinyTextType($characterSet, $collation);
                        break;
                    case 'mediumtext':
                        $dataType = new MediumTextType($characterSet, $collation);
                        break;
                    case 'longtext':
                        $dataType = new LongTextType($characterSet, $collation);
                        break;
                    case 'blob':
                        $dataType = new BlobType();
                        break;
                    case 'tinyblob':
                        $dataType = new TinyBlobType();
                        break;
                    case 'mediumblob':
                        $dataType = new MediumBlobType();
                        break;
                    case 'longblob':
                        $dataType = new LongBlobType();
                        break;
                }
            }
        }

        return $dataType;
    }
}

I am unsure of the most ideal pattern to clean this up. My current thoughts are:

  • Use something like the Chain Of Responsibility pattern, where I have a factory per data type, and each will decide if it can handle the data type string, and if it can't it will be passed onto the next factory.
  • Have a factory per data type, and a super factory that would examine the data type string to determine the correct data type factory to handle the string.

All examples of Factory patterns that I see are where the different objects all have the exact same constructor arguments. So I'm not sure of the the most sensible pattern to solve when the different objects have different constructor arguments.

Note, I originally posted this at on Stackoverflow at https://stackoverflow.com/questions/38339366/how-to-employ-the-factory-pattern-where-the-constructor-arguments-vary

\$\endgroup\$
  • \$\begingroup\$ As we all want to make our code more efficient or improve it in one way or another, try to write a title that summarizes what your code does, not what you want to get out of a review. Please see How to get the best value out of Code Review - Asking Questions for guidance on writing good question titles. \$\endgroup\$ – BCdotWEB Jul 13 '16 at 7:54
  • \$\begingroup\$ Your edited title is still too generic. What does your code do, e.g. "converting query results to PHP objects representing MySQL data types" or something like that. \$\endgroup\$ – BCdotWEB Jul 13 '16 at 8:11
  • \$\begingroup\$ Why do you need these types? What purpose they serve (some part of end user application or some dev tool/generator)? Are these "Data bags" with constraints? Do they share an interface? Give one class as an example. I feel that this is just a beginning of architectural mess which usually come from wrong approach to oop. \$\endgroup\$ – shudder Jul 14 '16 at 0:13
  • \$\begingroup\$ Thanks for your interest @shudder. I have expanded the details to explain what the purpose of this package is. You are correct in you guess that it is intended to be used with a dev-tool/generator. \$\endgroup\$ – Courtney Miles Jul 14 '16 at 7:19
  • \$\begingroup\$ Database oriented objects make sense if your app stays within database domain, and what these objects represent would be a response model, but i'm confused with cast() method. In my opinion it's inconsistent (SRP violation), but at the same time this is the only method that would make having subtypes reasonable (it could make a true object - not data structure). I mean you don't need special type if it's only responsibility is to tell its properties to the user. What is your intention behind this method? Who would be the client to such object? \$\endgroup\$ – shudder Jul 15 '16 at 4:33
1
\$\begingroup\$

My solution has been to create a factory class per data type, that conform to a common interface, and then have the factory method effectively map the mysql type to the appropriate factory class.

So the data type factor interface is as follows.

<?php

interface DataTypeFactoryInterface
{
    /**
     * @param $typeStr
     * @param null $collation
     * @return DataTypeInterface|bool
     */
    public static function create($typeStr, $collation = null);

    /**
     * @param $typeStr
     * @return bool
     */
    public static function doRecognise($typeStr);
}

As an example, here is the VARCHAR factory that implements this interface.

<?php

class VarcharTypeFactory extends AbstractStringTypeFactory
{
    /**
     * @param $typeStr
     * @param null $collation
     * @return VarCharType|bool
     */
    public static function create($typeStr, $collation = null)
    {
        if (!self::doRecognise($typeStr)) {
            return false;
        }

        return new VarCharType(self::getLength($typeStr), $collation);
    }

    /**
     * @param $typeStr
     * @return bool
     */
    public static function doRecognise($typeStr)
    {
        return preg_match('/^varchar\(\d+\)/i', $typeStr) === 1;
    }
}

And here is the Super Factory which is responsible for checking examining the MySQL data type and load the correct factory to handle it.

<?php

class Factory
{
    /**
     * @param $dataTypeString
     * @param null $collation
     * @return IntTypeInterface|NumericPointTypeInterface|OptionsTypeInterface|StringTypeInterface|null
     * @throws FactoryException
     */
    public function createDataType($dataTypeString, $collation = null)
    {
        $dataType = null;

        if (preg_match('/^(\w+)/', $dataTypeString, $matches)) {
            $namespace = 'MilesAsylum\Schnoop\Schema\MySQL\DataType\\';
            $factoryClass = $namespace . ucfirst(strtolower($matches[1])) . 'TypeFactory';

            if (class_exists($factoryClass)) {
                return $factoryClass::create($dataTypeString, $collation);
            } else {
                throw new FactoryException("A factory class was not found for the {$matches[1]} data type.");
            }
        }

        return $dataType;
    }
}

One catch with this is that I deviate from the strict CamelCase naming. I.e. Where my VARCHAR object is named VarCharType, the factory class is named VarcharTypeFactory (where the C is lower-case) because otherwise I would need to maintain a static mapping between the data type and the factory object.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.