1
\$\begingroup\$

I previously wrote the question about version 1 of my code, since then I have released a version 1.6.3. I would just like everyone's opinion on it. Here is the old question: FileMaker PHP API Interface

The code interacts with the FileMaker API for PHP which is the Official version of the FileMaker PHP API (not the fx.php by iViking).

This code helps users interact from the web to their FileMaker database. It does this by interacting with the FileMaker PHP API which retrieves XML from various requests to/from the server.

It uses the HTTP protocol to retrieve this data if I recall correctly.

The idea of this came from my Cybershade CMS project which can be found at http://github.com/cybershade/cmsv1 which is still under-development but it uses a simular syntax as this.

The main reason I made this class was to create an easier method for users to interact with the database.

Here is a link to the github

And here is the code directly:

<?php

require_once ( 'fm_api/FileMaker.php' );
require_once ( 'config/config.php' );

/**
 * Interface between the FileMaker API and PHP - Written By RichardC
 * 
 * @author  RichardC
 * @version 1.6.3
 * 
 * @license GPLv3
 */
class FMDB {
    /* 
    * Filemaker LessThan/Equal to and GreaterThan/Equal to characters
    * Does not work in all IDE's
    *
    * Update:
    *   Reason why I have defined these as a constant is because they will 
    *   never change. I have left them as a variable for those whom have already
    *   started using it as a variable 
    */
    const LTET = '≤';
    const GTET = '≥';

    /**
     * Setting up the classwide variables 
     */
    protected $fm,
              $layout = '',
              $debugCheck = true,
              $fieldList = array();

    public $revertedData = array(),
           $lastObj = null, 
           $ltet = self::LTET,
           $gtet = self::GTET;


    /** Constructor of the class */
    public function __construct() {
        //Performs all the relative checks that are required by the FM PHP API
        $this->doChecks(); 
        $this->fm = new FileMaker( FMDB_NAME, FMDB_IP, FMDB_USERNAME, FMDB_PASSWORD );
    }


    /**
     * Perform all checks before doing any thing
     * 
     * @todo    Will extend this function to perfrom more extensive tests
     * 
     * @author  RichardC
     * @version 1.0
     * 
     * @since   1.6
     * 
     * @return true
     */
    protected function doChecks(){
        if( !function_exists( 'curl_init' ) ){
            die( 'Please enable cURL to use the FileMaker PHP API' );
        }

        return true;
    }



    /**
     * Checks whether there is an error in the resource given.
     * 
     * @author  RichardC
     * @since   1.0
     * 
     * @version 1.6
     * 
     * @param   obj     $request_object
     * 
     * @return  int
     */
    public static function isError( $request_object ) {

        if( is_array( $request_object ) && preg_grep( '/^([^*)]*)error([^*)]*)$/', array_keys( $request_object ) ) ){
            foreach( $request_object as $key => $val ){
                return (int)$val;
            }
        }

        return ( FileMaker::isError( $request_object ) ? (int)$request_object->getCode() : 0 );
    }


    /** 
     * Just a quick debug function that I threw together for testing
     * 
     * @author  RichardC
     * @since   1.4
     * 
     * @version 1.4
     * 
     * @param   string  $func
     * @param   array   $arrReturn
     * @param   string  $type   'file' || 'console'
     * 
     * @return  mixed
     */
    protected function debug( $func, $arrReturn, $type='file' ){
        $debugStr = '';

        if( $func == '' || empty( $func ) ){
            return null;
        }

        $debugStr = '';

        switch( $type ){
            default:
            case 'file':

                $fo = fopen( DEBUG_LOCATION, 'a+' ); 

                foreach( $arrReturn as $k => $v ){

                    $v = ( is_array( $v ) ? $v : array( $k => $v ) );

                    foreach( $v as $n => $m ){
                        $debugStr .= sprintf(
                            '[Debug %s] %s - [ %s ] -> %s %s',
                            date( 'd-m-Y H:i:s' ),
                            $func,
                            $n,
                            $m,
                            "\n"
                        );
                    }
                }
                fwrite( $fo, $debugStr );
                fclose( $fo );

                return true;

                break;

            case 'console':
                foreach( $arrReturn as $k => $v ){
                    $v = ( is_array( $v ) ? $v : array( $k => $v ) );

                    foreach( $v as $n => $m ){
                        $debugStr .= sprintf(
                            '<script type="text/javascript"
                                console.log("[Debug] %s - %s -> %s ");
                            </script>',
                            $func,
                            $n,
                            $m
                        );
                    }
                }
                return $debugStr;
                break;
        }
    }

    /**
     * Simular to select but just returns the fields which you wanted
     * 
     * @todo    Figure out a way to reduce the amount of loops or make the loops faster
     * 
     * @warning This function is discouraged for a large amount of data due to the amount of loops
     * 
     * @author  RichardC
     * @since   1.6
     * 
     * @version 1.0
     * 
     * @param   string  $layout
     * @param   array   $arrSearchCriteria
     * @param   array   $arrFields
     * 
     * @return array
     */   
    public function getFields( $layout, $arrSearchCriteria, $arrFields ){
        $arrOut = array();

        if( !ctype_alnum( (string)$layout ) || !is_array( $arrSearchCriteria ) ){
            return $arrOut;
        }

        // If no fields are specified then perform a normal select 
        if( empty( $arrFields ) || !is_array( $arrFields ) ){
            return $this->select( $layout, $arrSearchCriteria );
        }

        // Perform the select
        $select = $this->select( $layout, $arrSearchCriteria );

        if( !$this->isError( $select ) ){

            // Loop through the returned fields
            foreach( $select as $field => $contents ){

                // Loop through the desired fields
                foreach( $arrFields as $f ){
                    if( $field == $f ){
                        $arrOut[$f] = $contents;
                    }
                }
            }
        } 

        return $arrOut;
    }


    /**
     * Selects data from a FileMaker Layout from the given criteria
     * 
     * @author  RichardC
     * @since   1.0
     * 
     * @version 1.4
     * 
     * @param   string  $layout
     * @param   array   $arrSearchCriteria
     *
     * @return  array
     */
    public function select( $layout, $arrSearchCriteria ) {
        $arrOut = array();

        if ( ( !is_array( $arrSearchCriteria ) ) ) {
            return false;
        }

        $findReq = $this->fm->newFindCommand( $layout );

        foreach ( $arrSearchCriteria as $field => $value ) {
            $findReq->addFindCriterion( $this->fm_escape_string( $field ), $this->fm_escape_string( $value ) );
        }

        $results = $findReq->execute();

        if ( $this->isError( $results ) === 0 ) {
            $fields = $results->getFields();

            $records = $results->getRecords();

            //Set the last used layout and object
            $this->layout = $layout;
            $this->lastObj = $records;

            //Loops through the records retrieved
            $i = 0;
            foreach ( $records as $record ) {
                $i++;
                foreach ( $fields as $field ) {
                    $arrOut[$i]['rec_id']   = $record->getRecordId();
                    $arrOut[$i][$field]     = $record->getField( $field );
                }
            }
        } else {
            $arrOut['errorCode'] = $this->isError( $results );
        }

        if( $this->debugCheck ){
            foreach( $arrOut as $k => $v ){
                $this->debug( 'SELECT', array(
                    $k  =>  $v 
                ));
            }
        }
        return $arrOut;
    }

    /**
     * Sets Fields within a given Layout with the given criteria
     * 
     * @author  RichardC
     * @since   1.0
     * 
     * @version 1.2
     * 
     * @param   array   $arrFields
     * 
     * @example $objFMDB->setFields( array( 'fieldName' => 'ValueToUpdate' ) );
     * 
     * @return  bool
     */
    public function setFields( $arrFields ) {
        $blOut = false;
        if ( ( !is_array( $arrFields ) ) ) {
            return false;
        }
        $layout = ( empty( $layout ) ? ( $this->layout ) : ( $layout ) );
        $records = $this->lastObj;

        if ( isset( $records ) && !empty( $records ) ) {
            foreach ( $records as $record ) {
                foreach ( $arrFields as $fieldName => $value ) {
                    $record->setField( $this->fm_escape_string( $fieldName ), $this->fm_escape_string( $value ) );
                }
            }
            $commit = $record->commit();
            if ( $this->isError( $commit ) === 0 ) {
                $blOut = true;
            } else {
                return $this->isError( $commit );
            }
        }

        // Housekeeping
        unset( $record, $commit, $fieldName, $value );

        return $blOut;
    }

    /**
     * Updates a record by the given ID of the record on a specified layout
     * 
     * @author  RichardC
     * @since   1.0
     * 
     * @version 1.2
     * 
     * @param   string  $layout
     * @param   array   $arrFields
     * @param   int     $iRecordID
     * 
     * @return  bool
     */
    public function updateRecordByID( $layout, $arrFields, $iRecordID ) {
        if ( ( $layout == '' ) || ( !is_array( $arrFields ) ) || ( !is_numeric( $iRecordID ) ) ) {
            return false;
        }
        $findReq = $this->fm->getRecordById( $layout, $iRecordID );

        if ( $this->isError( $findReq ) === 0 ) {

            foreach ( $findReq as $record ) {
                foreach ( $arrFields as $f => $v ) {
                    $record->setField( $this->fm_escape_string( $f ), $this->fm_escape_string( $v ) );
                }
                $commit = $record->commit();
            }

            if ( $this->isError( $commit ) === 0 ) {
                return true;
            } else {
                return $this->isError( $commit );
            }
        } else {
            return $this->isError( $findReq );
        }

        unset( $result, $commit, $record, $findReq );
        return false;
    }

    /**
     * Inserts a record into the layout
     * 
     * @author  RichardC
     * @since   1.0
     * 
     * @version 1.0
     * 
     * @param   string  $layout
     * @param   array   $arrFields
     * 
     * @return  bool
     */
    public function insert( $layout, $arrFields ) {
        $blOut = false;
        if ( ( $layout == '' ) || ( !is_array( $arrFields ) ) ) {
            return false;
        }

        // Auto-Sanitize the input data
        foreach ( $arrFields as $field => $value ) {
            $fields[$this->fm_escape_string( $field )] = $this->fm_escape_string( $value );
        }

        $addCmd = $this->fm->newAddCommand( $this->fm_escape_string( $layout ), $fields );
        $result = $addCmd->execute();

        if ( $this->isError( $result ) === 0 ) {
            $blOut = true;
        } else {
            return $this->isError( $result );
        }

        unset( $addCmd, $result );
        return $blOut;
    }

    /**
     * Gets the layout names within a Database
     * 
     * @author  RichardC
     * @since   1.0
     * 
     * @version 1.0
     * 
     * @return  array
     */
    public function get_layout_names() {
        return $this->fm->listLayouts();
    }



    /**
     * Updates a set of fields on a layout where the clauses match
     * 
     * @author  RichardC
     * @since   1.4
     * 
     * @version 1.0
     * 
     * @param   string  $layout
     * @param   array   $arrFields
     * @param   array   $arrSearchCriteria
     * 
     * @return  bool
     */
    public function update( $layout, $arrFields, $arrSearchCriteria ){

        //Loop through the parameters and check they are set and not empty
        foreach( func_get_args() as $arg ){
            if( ( $arg == '' ) || ( empty( $arg ) ) ){
                return false;
            }
        }

        $findReq = $this->fm->newFindCommand( $layout );

        foreach ( $arrSearchCriteria as $field => $value ) {
            $findReq->addFindCriterion( $this->fm_escape_string( $field ), $this->fm_escape_string( $value ) );
        }

        //Perform the find
        $result = $findReq->execute();

        if ( $this->isError( $result ) !== 0 ) {
            return $this->isError( $result );    
        }

        $records = $result->getRecords();

        //Loop through the found records 
        foreach ( $records as $record ) {

            //Loop through the fields given in the argument and set the fields with the values
            foreach ( $arrFields as $f => $v ) {
                $record->setField( $this->fm_escape_string( $f ), $this->fm_escape_string( $v ) );
            }

            //Commit the setFields
            $commit = $record->commit();

            if ( $this->isError( $commit ) !== 0 ) {
                return $this->isError( $commit );
            }
        }

        //Housekeeping
        unset( $result, $commit, $record, $findReq );

        return true;
    }


    /**
     * Alias of 'select' 
     * 
     * @author  RichardC
     * @since   1.0
     * 
     * @version 1.0
     * 
     * @param   string  $layout
     * @param   array   $arrSearchCriteria
     * 
     * @return  array
     */
    public function find( $layout, $arrSearchCriteria ) {
        return $this->select( $layout, $arrSearchCriteria );
    }


    /**
     * Runs a script on the layout
     * 
     * @author  RichardC
     * @since   1.0
     * 
     * @version 1.0.2
     * 
     * @param   string  $layout
     * @param   string  $scriptName
     * @param   array   $params (optional)
     * 
     * @return  bool
     */
    public function runScript( $layout, $scriptName, $params = array() ) {
        if ( ( empty( $layout ) ) || ( empty( $scriptName ) ) ) {
            return false;
        }

        return ( $this->fm->newPerformScriptCommand( $layout, $scriptName, $params ) ? true : false );
    }

    /**
     * Get the ID of the last updated/inserted field
     * 
     * @author  RichardC
     * @since   1.2
     * 
     * @version 1.0
     * 
     * @return  int
     */
    public function getLastID() {
    }

    /**
     * Deletes a record from the table/layout with the given record ID
     * 
     * @author  RichardC
     * @since   1.2.0
     * 
     * @version 1.0.2
     * 
     * @return  bool
     */
    public function deleteRecordByID( $layout, $iRecordID ) {
        $delete = $this->fm->newDeleteCommand( $layout, $iRecordID );
        $delResult = $delete->execute();

        if( $this->isError( $delResult ) ){
            return $this->isError( $delResult );
        }

        unset( $delete, $delResult, $layout, $iRecordID );
        return true;
    }


    /**
     * Deletes a record where the search criteria matches
     * 
     * @author  RichardC
     * @since   1.4.0
     * 
     * @version 1.0.0
     * 
     * @param   string  $layout
     * @param   array   $arrSearchCriteria
     * 
     * @return int      The amount of records deleted || errorCode
     */
    public function delete( $layout, $arrSearchCriteria ){
        if( empty( $layout ) || empty( $arrSearchCriteria ) ){
            return 0;
        }

        //Performs the search
        $search = $this->select( $layout, $arrSearchCriteria );

        if( empty( $search ) ){
            return 0;
        }

        //Checks for an error
        if( $this->isError( $search ) !== 0 ){
            return $this->isError( $search );
        }

        $i = 0;
        foreach( $search as $records ){

            $delete = $this->deleteRecordByID( $layout, $records['rec_id'] );

            // Errors return as strings so thats why the check is to make sure its an integer
            if( !is_int( $delete ) ){
                return $delete; //replace $delete with 0; after testing
            }
            $i++;
        }

        return $i;
    }

    /**
     * Gets the ID of the record in the last Select
     * 
     * @author  RichardC
     * @since   1.0
     * 
     * @version 1.0
     * 
     * @return  int
     */
    public function getRecordId() {
        return $this->lastObj->getRecordId();
    }

    /**
     * Escapes a string manually
     * 
     * @author  RichardC
     * @since   1.0
     * 
     * @version 1.0
     * 
     * @param   string  $input
     * 
     * @return  string
     */
    public function fm_escape_string( $input ) {
        if ( is_array( $input ) ) {
            return array_map( __method__, $input );
        }

        if ( !empty( $input ) && is_string( $input ) ) {
            return str_replace( 
                array( '\\', 
                        '/', 
                        "\0", 
                        "\n", 
                        "\r", 
                        "'", 
                        '"', 
                        "\x1a", 
                        '<', 
                        '>', 
                        '%00'
                ), array( 
                        '\\\\',
                        '\/', 
                        '\\0', 
                        '\\n', 
                        '\\r', 
                        "\\'", 
                        '\\"', 
                        '\\Z', 
                        '\<\\/', 
                        '\\/>', 
                        '' 
                ), $input );
        }
    }
}

?>

The format is nicer in the IDE, it got a bit messed up after being pasted, but please let me know what you think.

Thanks!

\$\endgroup\$
  • \$\begingroup\$ Feel free to read about What makes a god question on meta. A link to the previous question and an explanation of what the code does would help a lot. By the way, saying "I won't bother correcting my indentation" makes me want to say "I won't bother reviewing you code". \$\endgroup\$ – Quentin Pradet Mar 2 '12 at 12:46
  • \$\begingroup\$ Thanks for your comment. Fixed the issues you were talking about. Also linked to the new question. \$\endgroup\$ – DarkMantis Mar 2 '12 at 13:49
2
+50
\$\begingroup\$

Did you consider making this a REST API? Read more about The Good, the Bad, and the Ugly of REST APIs and REST APIs must be hypertext-driven from the creator of REST (this is one is a bit special, no need to follow it).

Aslo try explaning how your API is better. Perhaps that's obvious, but it would still be good to explain it a few points for someone just stopping by and wondering if he should use it or not.

Anyway, here are my observations. Feel free to discard them whenever you don't agree. I'd be glad to discuss in the comments:

  1. Your indentation is a bit funky, but at list seems consistent, so that's a good point.
  2. The ordering of the methods is important, especially when the reader has no IDE (and I think most PHP developers don't use IDEs), and especially for APIs that are intended to be used by third-party developers. Whenever your users will have errors, they will look at your code, so don't confuse them about internals such as doChecks or debug. (I don't even know what debug does right now.)
    1. Put protected functions last.
    2. Group public functions by "task".
    3. Try to put functions with no dependencies before, they will help understand the rest. (If the documentation for function X says "similar to Y", then Y should probably be placed before X. Same remark when X calls Y).
  3. isError should return a boolean
  4. The following code has unneeded parentheses. This is defensive programming, and I don't think it's good practice. If $arrSearchCriteria is not an array, you should fail hard, and not let this go unnoticed, since it will probably happen only during development.

    if ( ( !is_array( $arrSearchCriteria ) ) ) {
        return false;
    }
    
  5. I prefer handling error cases first, even more so when they are really short. It helps knowing that the else is the normal case. When putting the error condition last, you can not see it and have to remember "maybe there's something else", which is cumbersome. For example:

    if ( $this->isError( $results ) === FALSE ) {
        $arrOut['errorCode'] = $this->isError( $results );
    } else {
        // normal stuff.
    }
    

    (And keep in mind the "Flattening Arrow Code" comment from palacsint on your earlier question. :)

  6. if ( isset( $records ) && !empty( $records ) ) { same comment than 4. You have other occurrences of this issue, I won't list them all.
  7. // Housekeeping : strive for meaningful comments. :)
  8. What is $blOut?
  9. Is this useful or confusing? Try adopting a convention for your names, to allow users to easily guess them. Changing names arbitrarily is not nice. If you want to code_like_this, at least use list_layouts.

    public function get_layout_names() {
        return $this->fm->listLayouts();
    }
    
  10. public function getLastID() { } Oops?
  11. @return int The amount of records deleted || errorCode Use exceptions!
  12. What makes your string escaping function special? Why not used a PHP function? Does your function has bugs or flaws?
\$\endgroup\$
  • \$\begingroup\$ Fantastic answer, thanks. #4/6 - I agree, I obviously didn't proof read my code enough to notice that. #7 - I will change this; #8 - Strange you should mention that, it was a coding standard that I got from my previous company and it is the boolean return variable but I know a lot of other developers hate it; #10 - Still not finished yet, it is still a work in progress as you can see. #12 - The only reason I use that is because its not using MySQL so I cannot use mysql_real_escape_string() but could use addslashes, htmlentities etc (Point taken). \$\endgroup\$ – DarkMantis Mar 2 '12 at 17:20
  • \$\begingroup\$ I do not like the "Flattening Arrow Code", in my opinion it makes it more difficult to read, but that is just a personal opinion. As for making this a RESTful API, I don't know too much about REST to start but I will look into it as I do love the idea of REST as I say I just don't know where to begin. Thanks again for all of your suggestions, it's much appreciated! \$\endgroup\$ – DarkMantis Mar 2 '12 at 17:22
  • \$\begingroup\$ I will give you my bounty btw, but I have to wait 18 hours to do so :) \$\endgroup\$ – DarkMantis Mar 2 '12 at 17:23

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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