6.0.0-git
2024-05-06
Last Modified 2004-08-03 by Jan Schneider

Proposal for Implementation of New Package to Replace Horde_Form_Type:: and Horde_Form_Variable::

Notes

IRC Log From 2004-02-13


<oo> trying to figure out the best way to enable editing of images

                using horde_form image type

<oo> because at the moment form data being edited and containing an

                image field type, drops the image

<oo> the idea i'm following right now would be that the image data

                is copied from the vfs store to the /tmp dir for

                display/modification by the image field. then upon submission

                copied back.

<cjh> seems reasonable enough...

<cjh> the type should be able to create the /tmp file, and then

                 return the final data, but not do the final write-back,

                 right?

<oo> only thing is, this would have to be triggered by the

                renderer. because the form type doesn't know about the old

                data being loaded until it is rendered.

<oo> right

<cjh> hmm.

<cjh> I'm not entirely happy with the VarRenderer setup right now.

<cjh> we still have the functions for rendering all the types off

                 in one file, which seems clunky

<cjh> and there are too many places you need to edit to add a new

                 type - not intuitive at all.

<oo> i was just talking about bracking up horde_form into multiple

                files with jan earlier today

<oo> braking even

<oo> the hunch being that for each form you only need a handful of

                types right?

<cjh> breaking :)

<cjh> right.

<oo> which *could* be better efficiency that including 100k+ files

<cjh> yeah. and there are probably other places we can reduce

                 overhead in the form libs.

<oo> hm, no wonder "braking" looked weird too :)

<cjh> like, do we really need Horde_Form_Type *and*

                 Horde_Form_Variable? might be more efficient have variables

                 just be instances of types.

<cjh> (instead of having two instantiated objects for each)

<oo> i think it could all be folded into horde_form_variable

<cjh> huh, I was thinking the other way around :)

<oo> dunno... either way i guess. was just following some language

                logic

<cjh> true, yeah.

<cjh> eh, I still think Type makes more sense? dunno.

<cjh> variables have types, but ...

<oo> :)

<Eraserhd> back

<Eraserhd> Can I put in my $.02 re Horde_UI_VarRenderer:: ?

<oo> no, it will cost you $.04...

<oo> inflation

<Eraserhd> hehe.   I agree with pretty much everything.  I keep thinking that

           we have too many types.  We need to coalesce different

           representations of the same type.

<oo> hm?

<Eraserhd> For example, 'enum' and 'radio' are really different presentations

           of the same type.

<Eraserhd> And 'multienum' and 'checkboxes'.

<Eraserhd> And the various different date fields.

<Eraserhd> Part of the reason I think that, though, is because we have a

           similar thing in our old framework, but it has slightly different

           semantics which make it much more useful.  They represent *data*

           types not *field* types in our system, and the result is that a

           data type knows how to draw itself, accept form input, accept

           "query" input (like a range for the value for ints or a keyword

           expression for strings).

<Eraserhd> We can pass options to field type and construction type and we use

           that for different representations.

<cjh> I kind of think that we need to start this as a new package of some

          sort, so that we can develop it and *then* selectively migrate

          existing apps to it.

<Eraserhd> er s/and construction type/at construction time/

<cjh> (using our packages to not break BC :)

<cjh> but I agree that it could be organized more usefully.

<Eraserhd> works for me.

<Eraserhd> Would my login work for the wiki?

<Eraserhd> I want to post my Field:: class and a derived class for examples of

           what to do (and what not to do :).

<Eraserhd> The other thing was that I was thinking about this export

           definition thing, and I think you should be able to query a

           Field::-derived class for different representations (e.g. Do we

           want to export as UNIX timestamp or mm/dd/yyyy?)

<cjh> you can do that with the date type in Horde_Form now, I believe.

<oo_> correct

<oo_> plus don't know what i missed but enum/radio are only different in how

          they are rendered.. in the horde_form_type classes one inherits from

          the other

Jay's Field:: Class from Cadre Framework

This is an example from the Cadre framework. I'm certainly not suggesting just taking this and using it, but this class has weathered almost four years of service, so I'm posting it here for discussion in this context.

Here are some notes:

  • The Get_Field() function at the end is equivalent to Horde's ::singleton() method.
    • Data describing the field is stored in a table in the database. Fields are loaded by name. We design our database schemas so that all fields have unique names (we prepend the field name with the table name, except with foreign keys where we use the same name as the referenced field). This way we can just say to our framework "get me a user_id" and it will return a class which can:
      • Render the value as text (::Get_Display_Text()), HTML (::Get_Display_Html()), or TeX (::Get_Display_TeX())
      • Render the value as an HTML field for input. (::Get_Edit_Html())
      • Convert the HTML representation back into the data representation (::Get_Db_Value()).
      • Render a search field for the value (::Get_Filter_Html()). For a date field, this might render a start date and end date fields.
      • Convert the HTML filter representation into an SQL filter expression. (In the previous example, it might return "$field_name >= <date> AND $field_name <= <date2>". If all blank, it will return "TRUE". (::Get_Filter_SQL()).
      • Validate the field input and return an error message if it doesn't pass (::Validate()).
    • "Options" and "Flags" control how the field behaves, and these are stored in the database. You can override them when calling Get_Field() if necessary.


/* CADRE - Cronosys web application framework.

 * Copyright (C) 2002 Cronosys, LLC.

 *

 * $Id: class.Field.php,v 1.23 2004/01/22 13:52:39 jasonf Exp $

 * 

 * This program is free software; you can redistribute it and/or modify

 * it under the terms of the GNU General Public License as published by

 * the Free Software Foundation; either version 2, or (at your option)

 * any later version.

 * 

 * This program is distributed in the hope that it will be useful,

 * but WITHOUT ANY WARRANTY; without even the implied warranty of

 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the

 * GNU General Public License for more details.

 * 

 * You should have received a copy of the GNU General Public License

 * along with this software; see the file COPYING.  If not, write to

 * the Free Software Foundation, Inc., 59 Temple Place, Suite 330,

 * Boston, MA 02111-1307 USA */

/**

 * The field class is an abstract base class which encapsulates a

 * particular type of field.

 */

class Field {

    //{{{ properties

    var $name;

    /**

     * Array of information about this field, typically fetched from the

     * <database>fw_fields</database> table.

     */

    var $info;

    /**

     * This is a name => value array of field options.  This is parsed from

     * field_type or optionally passed in the constructor.

     * @var array $options

     */

    var $options = array();

    //}}}

    //{{{ constructor

    /**

     * Construct a new field object.

     *

     * @param array $info           This is a key => value array of items

     *                              describing the field.

     * @param array $addtl_options  These are additional options which override

     *                              any options found in $info['field_type'].

     */

    Function Field ($info, $addtl_options = array())

    {

    $this->info = $info;

    $this->name = $this->info["field_name"];

        if (isset($info['field_type']))

            {

            $work = $info['field_type'];

            while (strlen($work) > 0)

                {

                if (preg_match("/^([a-zA-Z0-9_]+)(?:(=)(\"(?:[^\"\\\\]|\\\\.)*\"|[^,]*))?,?/", $work, $m))

                    {

                    if (isset($m[2]) && $m[2] == '=')

                        {

                        if (substr($m[3],0,1) == '"' && substr($m[3],-1) == '"')

                            {

                            $value = preg_replace('/\\\\(.)/', '\\1',

                                                  substr($m[3],1,

                                                         strlen($m[3])-2));

                            }

                        else

                            $value = $m[3];

                        }

                    else

                        $value = true;

                    $this->options[$m[1]] = $value;

                    }

                else

                    break;

                $work = substr($work, strlen($m[0]));

                }

            }

        $this->options = array_merge($this->options, $addtl_options);

    }

    //}}}

    //{{{ Display_Name()

    /**

     * This method returns a name for the field as it would appear to a

     * user on a report or an entry screen.  For example, for the database

     * field <database>user_name</database>, it might return "User Name".

     *

     * @returns the field's display name.

     */

    Function Display_Name ()

    {

    return $this->info["field_display_name"];

    }

    //}}}

    //{{{ Has_Field_Type_Flag()

    /**

     * Determines if <varname/$flag/ is one of the comma-separated values in

     * this field's <database/field_type/ field.

     *

     * @param $flag the flag to check for.

     * @returns true if the flag is present, false otherwise.

     */

    Function Has_Field_Type_Flag ($flag)

    {

        return isset($this->options[$flag]) && $this->options[$flag] !== false;

    }

    //}}}

    //{{{ Get_Option()

    /**

     * Retrieves the value of an option as supplied in the `field_type' flag

     * from the database.

     *

     * @param string $name          This is the option's name.

     * @param mixed $default        This is the deafult value of the option.

     * @return mixed the option's value or null if not present.

     */

    Function Get_Option ($name, $default = null)

    {

        return isset($this->options[$name]) ? $this->options[$name] : $default;

    }

    //}}}

    //{{{ Get_Db_Value()

    /**

     * This method converts a representation of the field's value as it

     * would be received from an HTML form to a form acceptable for the

     * database.

     *

     * For example, a boolean field might appear on an edit form as a

     * checkbox and return the value "On" if the box is checked.  For a 

     * boolean field type, this method would convert "On" to "t", which is

     * the way the PostgreSQL database likes to receive its boolean true.

     *

     * This method is not responsible for quoting or escaping the value.

     *

     * @param value the value received from the HTML form.

     * @returns the value you would send to the database.

     */

    Function Get_Db_Value ($value)

    {

    if (isBlank($value) && $this->Has_Field_Type_Flag('nullable'))

        return null;

    return $value;

    }

    //}}}

    //{{{ Get_Filter_Value()

    /**

     * This method converts the HTTP post data created by the HTML from 

     * <function/Field::Get_Filter_Html()/ to a "database" value suitable

     * for passing around and/or sending back to <function/Get_Filter_Html()/.

     *

     * @param value the value received from the HTML form.

     * @returns the value you would pass around.

     */

    Function Get_Filter_Value ($value)

    {

        return $this->Get_Db_Value ($value);

    }

    //}}}

    //{{{ Get_Filter_SQL()

    /**

     * This method converts a filter value to SQL code to filter a query. The

     * value here is not the post value, but the value as might be returned

     * by <function/Field::Get_Filter_Value()/.

     *

     * By default, we work like a text field with a keyword search.

     *

     * @param $value the filter value to convert.

     * @returns an expression useful for an SQL "WHERE" clause.

     */

    Function Get_Filter_SQL ($column, $value, &$error_message)

    {

        if (is_null($value))

            return "TRUE";

    $ret = dbKeywordClause($column, $value, $error_message);

    if (!isBlank ($error_message))

        $error_message .= ' in "'.$this->Display_Name() .'"';

    return $ret;

    }

    //}}}

    //{{{ Get_Display_Text()

    /**

     * This method translates the value into a textual representation that

     * you might display to a user.  This method does not use HTML or any

     * other sort of markup; for that, see the 

     * <function>Get_Display_Html</function> method.

     *

     * @param value the value retreived from the database.

     * @returns a human-readable, plaintext representation of the value.

     */

    Function Get_Display_Text ($value)

    {

    return $value;

    }

    //}}}

    //{{{ Get_Display_Html()

    /**

     * This method translates a database value into an HTML representation

     * that you might display to a user.  This method uses HTML markup for

     * formatting if necessary.

     *

     * Implementors of this method in derived classes should not that any

     * HTML special characters should be properly escaped with 

     * <function>htmlspecialchars</function>().

     *

     * @param value the value retreived from the database.

     * @returns a human-readable, marked-up representation of the value.

     */

    Function Get_Display_Html ($value)

    {

    $value = $this->Get_Display_Text ($value);

    if ( IsBlank ($value) )

        return "&nbsp;";

    else

        return htmlspecialchars ($value);

    }

    //}}}

    //{{{ Get_Display_TeX()

    /**

     * This method translates a database value into an TeX representation

     * that you might display to a user.  This method uses TeX markup for

     * formatting if necessary.

     *

     * Implementors of this method in derived classes should ensure that the

     * returned string is properly escaped for TeX output or call this base

     * implementation.

     *

     * @param value the value retreived from the database.

     * @returns a human-readable, marked-up representation of the value.

     */

    Function Get_Display_TeX ($value)

    {

    $ret = '';

    $value = $this->Get_Display_Text ($value);

    return texspecialchars($value);

    }

    //}}}

    //{{{ Get_Browse_Display_TeX()

    Function Get_Browse_Display_TeX ($value)

    {

    return $this->Get_Display_TeX ($value);

    }

    //}}}

    //{{{ Get_Browse_Display_Html()

    /**

     * This method translates a database value into an HTML representation

     * in much the same way that <function>Get_Display_Html</function>() does;

     * however, this method may truncate the data or otherwise abbreviate it.

     * This is designed to use from <classname>Browser</classname> tables.

     *

     * @param value the value retreived from the database.

     * @returns a human-readable, marked up representation of the value.

     */

    Function Get_Browse_Display_Html ($value)

    {

    return $this->Get_Display_Html ($value);

    } // Function Get_Browse_Display_Html ()

    //}}}

    //{{{ Get_Edit_Value()

    /**

     * This method retreives the most appropriate value for the field.  Note

     * that if validation failed and the <varname/$base/ doesn't start with

     * `post' or `view', it won't get the proper value.  That's okay, because

     * everything should be under `post' or `view' anyway.

     */

    Function Get_Edit_Value ($value, $base = 'post[data]')

    {

    global $post, $view, $store;

    $postvar = $base."[".$this->info[field_name]."]";

    if ( $post[validation_failed] )

        eval("\$value = \$$postvar;");

    elseif ( IsSet($store[$postvar]) )

        $value = $store[$postvar];

    return ($value);

    }

    //}}}

    //{{{ Get_Edit_Html()

    /**

     * This method translates a database value into an editable HTML

     * representation.  It generates the necessary form control or controls

     * and populates them with <parameter>$value</parameter>.

     *

     * @param value the current value of the field.

     * @param base a hack to allow more than one field with the same name

     *        on one form.

     * @returns the HTML for the edit controls.

     */

    Function Get_Edit_Html ($value, $base = 'post[data]')

    {

    $postvar = $base."[".$this->info[field_name]."]";

    $value = $this->Get_Edit_Value($value, $base);

    return "<INPUT TYPE=\"TEXT\" NAME=\"$postvar\" VALUE=\"" .

        htmlspecialchars ($value) . "\" SIZE=\"" .

        $this->info["field_width"] . "\"" . ( $this->Has_Field_Type_Flag("small") ? " class=\"smalledit\"" : "" ) . ">";

    }

    //}}}

    //{{{ Get_Filter_Html()

    /**

     * This method translates a database value into editable HTML control(s)

     * of the sort you would use to search based on that field as opposed

     * to the sort you would use to edit a value in that field.  This allows

     * us to search on a date range for a date field, for example.

     *

     * @param $value the default filter value.

     * @param $base the variable's base.

     * @returns the HTML for the edit control(s).

     */

    Function Get_Filter_Html ($value, $base = 'view[filter]')

    {

    return $this->Get_Edit_Html ($value, $base);

    }

    //}}}

    //{{{ Validate()

    /**

     * This method is called before processing post data to determine if the

     * HTML form value is valid.  It is meant to be overridden by date types,

     * for example, to check that the date format is valid before passing

     * the information to the database.

     *

     * @param value the HTML form value to validate.

     * @returns an empty string if the input is valid; otherwise, a friendly

     *        error message.

     */

    Function Validate ($value)

    {

    return "";

    }

    //}}}

    //{{{ Database_Interface()

    /**

     * This returns the interface that we use for dispatching field types.

     */

    Function Database_Interface ()

    {

?>

    <table name="fw_field">

    <field name="field_name" type="text" allowNulls="false"/>

    <field name="field_display_name" type="text"/>

    <field name="field_edit_type" type="text" allowNulls="false"/>

    <field name="field_width" type="integer"/>

    <field name="field_options" type="text"/>

    <index type="unique">

        <indexField name="field_name"/>

    </index>

    </table>

<?php

    }

    //}}}

}

//{{{ Get_Field()

/**

 * Finds a field in the fields table, determines it's class, and instantiates

 * the appropriate class for that field.  It is assumed that the class name

 * in the database is the named of a class derived from the Field class.  The

 * class must already exist or be in a file named 

 * <filename>class.<replaceable/classname/.php</filename>.

 *

 * If the field does not exist in the database, some reasonable guesses are

 * made - a bare Field class is implemented, the field's caption is set to

 * $name.

 *

 * The return value is cached, so calling this multiple times for the same

 * field isn't a performance issue.

 *

 * @param string $name          the name of the field.

 * @param array $field_options  allows a screen to send custom info to a

 *                              field object.

 * @returns an instance of a field type class.

 */

Function Get_Field ($name, $field_options = array())

{

    global $db;

    global $field_cache;

    if ( !IsBlank ($field_cache[$name]) )

    return $field_cache[$name];

    $r = $db->Query ("SELECT * FROM fw_field

    WHERE field_name = '" . addslashes ($name) . "';");

    if ( !$r || $r->Row_Count () <= 0 )

    {

    $info = array (

        "field_name" => $name,

        "field_display_name" => $name,

        "field_displayable" => "t",

        "field_edit_type" => "Field",

        "field_width" => 20

        );

    }

    else

    $info = $r->Fetch_Row (0);

    if ( $r )

    $r->Free ();

    if ( IsBlank ($info["field_edit_type"]) )

    $info["field_edit_type"] = 'Field';

    include_once "class.$info[field_edit_type].php";

    $field_cache[$name] = new $info['field_edit_type'] ($info, $field_options);

    return $field_cache[$name];

}

//}}}

Email from 5/26/04:


Date:     Wed, 26 May 2004 14:59:25 -0400 [02:59:25 PM EDT]

From:     Chuck Hagenbuch

Subject:  [dev] Horde_Form_Type, VarRenderer, and what to do about it all.

I'm going to take the opportunity of a Text_Wiki design discussion on the PEAR

lists to propose we reorganize our whole concept of varrenderers and form

types, etc.

First of all, we should clean up the form types so that they are solely

conceptual - no display assumptions/logic/differences in them. No difference

between radio and select lists, for example.

Then we have the field renderers/etc., one renderer per type of field (where

radio and select lists *are* different) per rendering format supported (XHTML,

PDF, etc).

It'd be a lot of files, which I've tried to avoid in the past, but it seems the

only way to really make this flexible, and to avoid having to parse *too* much

code just to get a select widget, for example. This should be useable with

things like Horde_UI_Table, the Prefs system, the Blocks configuration, etc.,

too, without intrisically requiring Horde_Form.

I'm not sure where the renderers should live in the class tree.

The Text/Wiki class tree idea that got me thinking about this again looks like

so:

        Text/Wiki/Parse.php

        Text/Wiki/Parse/Bold.php

        Text/Wiki/Parse/Code.php

        Text/Wiki/Parse/Wikilink.php

        Text/Wiki/Render.php

        Text/Wiki/Render/DocBook.php

        Text/Wiki/Render/DocBook/Bold.php

        Text/Wiki/Render/DocBook/Code.php

        Text/Wiki/Render/DocBook/Wikilink.php