Git Product home page Git Product logo

afform's People

Contributors

colemanw avatar eileenmcnaughton avatar mlutfy avatar totten avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

afform's Issues

Update php version

Php version is set to 5.4+ - we support 5.6 as a minimum for core (although it's OK for extensions to have a more aspirational bottom limit)

Afform Auditor: Defining schemas

The auditor is a component which identifies issues in stored forms - which, in turn, involves two general areas:

  1. Schema/ruleset design - How do you define "valid" and "invalid"? (Ex: Create a whitelist of supported tags.)
  2. User/developer experience - When should the forms be checked for validity, and how should this be communicated? (Ex: Check the rulesets after any extensions are upgraded.)

This issue is specifically focusing on the schema/ruleset side.

Example Rules and Mental Model

Before we dig into specific tools or algorithms, let's consider some rules we'd like to articulate in English:

  • Any tag, attribute, or CSS-class that isn't explicitly whitelisted should generate a warning.
  • Any afform created by an extension or via API becomes a component/directive/tag that is available for use in others.
  • For forms customized in the GUI, the <af-model-list> must be present in the top-level form.
  • The following standard HTML tags are allowed for general organization:
    • div span p h1 h2 h3 h4 h5 h6 fieldset label pre blockquote
  • The following standard HTML tags are allowed for styling of text: a strong em tt code sub sup
  • The following standard HTML tags should raise a warning:
    • b i center
  • The following standard Angular directives are allowed anywhere:
    • ng-if ng-show ng-hide ng-classes
  • The following standard Angular directives are allowed on links and buttons:
    • ng-click
  • The following Afform directives are allowed for working with data:
    • af-model-list af-model-prop af-model af-field aff-api4-action
  • The following BootstrapCSS classes are allowed, provided they are put on <a> or ` tags:
    • .btn .btn-default .btn-primary .btn-danger

In considering these rules, it stands out to me that we have distinct sets of rules which can be combined. Thus, the "set of rules for user-editable forms" is the result of combining the "set of rules for basic HTML" plus "set of rules for basic AngularJS" plus "set of rules for BootstrapCSS" plus "set of rules for Afform data-handling".

Existing Standards

Tools for validating SGML/HTML/XML have been around as long as SGML/HTML/XML have been around. There are three widely used standards for defining XML rules: Document Type Definition (DTD), XML Schema Definition (XSD), and RelaxNG (RNG).

These three systems have an obvious strength: there are many tools, libraries, tutorials, books, stackexchange questions, etc which deal with them. You will find many examples of how to create the schema for a document like this:

<library>
   <book>
     <author><name>Lewis Carroll</name><dob>1832-01-27</dob></author>
     <title>Alice in Wonderland</title>
   </book>
</library>

I started refreshing myself a bit on these - and, in particular, I liked this RelaxNG book. Two interesting things:

  • RelaxNG supports a "compact" notation - which I find more readable than DTD or XSD.

  • RelxaNG is positioned as the more extensible of the three. For example, consider this snippet (chap 10): an upstream provider has defined an original schema in library.rnc, and a downstream consumer creates a custom variant of the schema in which element-name (i.e. any <name> tag) must have text content with less than 80 characters.

    include "library.rnc" {
       element-name = element name { xsd:token{maxLength = "80"} }
    }
    

So... can we address the bulk of Afform validation by delegating out to a standard library and making a few XML config files? I initially hoped so, but I'm starting that something more is needed:

  • The developer-stories for DTD, XSD, and RNG all begin with creating a new dialect top-down. This feels right if you're designing the contract for a web-service. But in this case, we're actually appropriating an existing dialect (HTML) and mixing-in a set of changes based on policy/configuration/data. RNG is more plausible here than DTD, but even there it feels like there's something missing.
  • IMHO, there's a functional need around HTML class validation. I haven't heard of or imagined a way to use any common XML schema standard to effectively model HTML class constraints. (CSS notation does this better...)
  • The af-field, af-model, af-model-prop should express field-names/entity-names/entity-types which match. I can see how to enforce this with, eg, PHP logic - but I'm struggling to see how to express those constraints.
  • To my understanding, the validators are rather binary - either the document passes or fails. In reality, I think it's valuable to support shades-of-grey like "X is deprecated" or "Y is experimental".

I think it's worth verbalizing a bit about other ways to organize rules.

Concept: CSS-like Validation DSL

In this pseudocode sketch, the basic concept is to list selectors and apply some policy to the match elements. It resembles CSS. To wit: Given a selector (h1,h2,h3 or a.btn), mark the identified elements as valid/OK. Or, given a selector, mark the matching elements with a warning. Or... call a PHP function to evaluate each matching element.

@define html-content {
  div, span, p, h1, h2, h3, h4, h5, h6, blockquote, pre {ok}
}
@define html-style {
  strong, em, tt, code, del, sub, sup, cite {ok}
  b, i, strike, center {
    /* "ok" above was a special short-hand for "$this->ok();", but generally... these are PHP blocks */ 
    $this->warn('The old school layout tags are deprecated. Use a semantic tag like <strong> or <em>.');
  }
}
@define afform-data {
  af-model-list {ok}
  af-model-prop, af-model-prop[name], af-model-prop[type] {ok}
  af-model, af-model[name] {ok}
  af-field, af-field[field-name] {ok}
  af-model-prop { 
    /* Run the PHP code on each matching element */
    static $entityTypes = Civi\Api4\Entity::get()->addSelect('name')->execute()->indexBy('name');
    if (!isset($entityTypes[$this['type']])) {
      $this->warn(ts('Unknown entity type!'));
    }
  }
  af-model { checkAfformModelName($this) }
}
@define bootstrap-style {
  .btn {
    if ($this->getName() == 'a' || $this->getName()  == 'button') { $this->ok(); }
    else { $this->warn(ts('Bootstrap buttons must use <A> or <BUTTON>.')); }
  }
  .btn-default, .btn-primary, ... {
    if ($this->hasClass('btn')) { $this->ok(); }
    else { $this->warn(ts('Bootstrap button decorators must be used with "btn".')); }
  }
}

/** A user-editable form may contain a mix of some HTML, some Angular, and some BootstrapCSS */
@define afform-gui-editable {
  @include html-content, html-style, afform-data, bootstrap-style
}

What I like about this: The DSL is concise. If you know CSS and a little PHP, then it should be fairly easy to read. (The Github CSS syntax highlighter even does a nice job despite some non-CSS bits.) CSS provides notations for tag-elements, attributes, and HTML classes.

What I don't like about this: Using a DSL requires more parsing work. If one needs to generate rules programmatically (e.g. via hook), then you have to go understand another notation.

Concept: Array of Selector/Action Rules

In this sketch, we avoid the need to implement a DSL. Just expose an array data-structure.

/**
 * @var array $rulesets
 *  Ex: $rulesets['myrule'][] = ['match' => string $cssSlector, 'call' => mixed $callable];
 */
$rulesets = [
  'html-content' => [
    ['match' => 'div, span, p, h1, h2, h3, h4, h5, h6', 'call' => Auditor::OK],
  ],
  'html-style' => [
    ['match' => 'strong, em, tt, code, del, sub, sup, cite', 'call' => Auditor::OK],
    ['match' => 'b, i, strike, center', 'call' => function($ctx) {
      $ctx->warn(ts('The old school layout tags are deprecated. Use a semantic tag like <strong> or <em>.'));
    }],
  ],
  // et al...
];

What I like: It's amenable to hooking and merging; it can be amenable to serialization (depending on what callback notations are allowed). It's easy to imagine adding more metadata to each rule (like a weight or a symbolic name).

What I don't like: The array-structure gets fairly deep and doesn't document itself.

Concept: Fluent Rule Builder

In this sketch, it uses the same mental model as the other two (match a CSS selector; specify a callback function). However, it uses a fluent OOP style to build the rules. Some of the fluent functions (ok($cssSelctor) or warn($cssSelector, $message)) are short-cuts for registering callback functions.

$rulesets = Civi::service('afform_rule_sets');

$rulesets->define('html-content')
  ->ok('div, span, p, h1, h2, h3, h4, h5, h6');

$rulesets->define('html-style')
  ->ok('strong, em, tt, code, del, sub, sup, cite')
  ->warn('b, i, strike', ts('The old school layout tags are deprecated. Use a semantic tag like <strong> or <em>.'))
  // Or... an equivalent but more general-purpose notation...
  ->call('b, i, strike', function($ctx){
      $ctx->warn(ts('The old school layout tags are deprecated. Use a semantic tag like <strong> or <em>.'));
  })

What I like: You get better IDE support (autocomplete/drilldown).

What I don't like: The canonical form is PHP code that's hard to serialize/transmit. Adding in weights and symbolic-names may not be as pretty.

Use limited API keys to escalate permissions using form metadata

For example, Civi\API\WhitelistRule defines a model for limiting permissions in an API. However, what we need is a way for the form-definition to embed grant statements. (Note: The metadata can be stored/read/interpreted server-side.)

A few bits of pseudocode (brainstorming) for how this might work:

<form>
  <div api3-grant="{entity:'Contact', actions:'create', fields:'*', required:[contact_id: '@user_contact_id']]"></div>
  <button api3-click="['Contact', 'create', myparams]">Save</button>
</form>
<form>
  <button
    api3-click="['Contact', 'create', myparams]"
    api3-authz="{fields:'*', required:[contact_id: '@user_contact_id']}"
  >
    Save
  </button>
</form>
<form>
  <button
    api3-click="['Contact', 'create', myparams]"
    api3-authz="{fields:'first_name,last_name', required:[contact_id: '@user_contact_id']}"
  >
    Save
  </button>
</form>

API4 explorer currently broken by afform

With afform enabled at the moment api v4 explorer fails with a 500 error due to the getfields action being missing I think (I realise this is all in a moving -parts state so it might be temporary anyway)

Civi\API\Exception\NotImplementedException::__set_state(array(
   'extraParams' => 
  array (
    'error_code' => 'not-found',
  ),
   'message' => 'Api Afform getFields version 4 does not exist.',
   'string' => '',
   'code' => 0,
   'file' => '/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/Civi/Api4/Generic/AbstractEntity.php',
   'line' => 53,
   'trace' => 
  array (
    0 => 
    array (
      'file' => '/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/api4.php',
      'line' => 30,
      'function' => '__callStatic',
      'class' => 'Civi\\Api4\\Generic\\AbstractEntity',
      'type' => '::',
      'args' => 
      array (
        0 => 'getFields',
        1 => 
        array (
        ),
      ),
    ),
    1 => 
    array (
      'file' => '/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/Civi/Api4/Action/Entity/GetFields.php',
      'line' => 20,
      'function' => 'civicrm_api4',
      'args' => 
      array (
        0 => 'Afform',
        1 => 'getFields',
        2 => 
        array (
          'action' => 'getFields',
          'includeCustom' => true,
          'select' => 
          array (
          ),
        ),
      ),
    ),
    2 => 
    array (
      'file' => '/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/Civi/Api4/Provider/ActionObjectProvider.php',
      'line' => 79,
      'function' => '_run',
      'class' => 'Civi\\Api4\\Action\\Entity\\GetFields',
      'type' => '->',
      'args' => 
      array (
        0 => 
        Civi\Api4\Generic\Result::__set_state(array(
           0 => 
          array (
            'name' => 'Activity',
            'description' => 'Activity entity.',
            'comment' => 'An activity is a record of some type of interaction with one or more contacts.',
            'fields' => 
            array (
              0 => 
              array (
                'default_value' => NULL,
                'name' => 'id',
                'title' => 'Activity ID',
                'entity' => 'Activity',
                'description' => 'Unique  Other Activity ID',
                'required' => true,
                'options' => false,
                'data_type' => 'Integer',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              1 => 
              array (
                'default_value' => NULL,
                'name' => 'source_record_id',
                'title' => 'Source Record',
                'entity' => 'Activity',
                'description' => 'Artificial FK to original transaction (e.g. contribution) IF it is not an Activity. Table can be figured out through activity_type_id, and further through component registry.',
                'required' => false,
                'options' => false,
                'data_type' => 'Integer',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              2 => 
              array (
                'default_value' => '1',
                'name' => 'activity_type_id',
                'title' => 'Activity Type ID',
                'entity' => 'Activity',
                'description' => 'FK to civicrm_option_value.id, that has to be valid, registered activity type.',
                'required' => true,
                'options' => true,
                'data_type' => 'Integer',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              3 => 
              array (
                'default_value' => NULL,
                'name' => 'subject',
                'title' => 'Subject',
                'entity' => 'Activity',
                'description' => 'The subject/purpose/short description of the activity.',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              4 => 
              array (
                'default_value' => NULL,
                'name' => 'activity_date_time',
                'title' => 'Activity Date',
                'entity' => 'Activity',
                'description' => 'Date and time this activity is scheduled to occur. Formerly named scheduled_date_time.',
                'required' => false,
                'options' => false,
                'data_type' => 'Timestamp',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              5 => 
              array (
                'default_value' => NULL,
                'name' => 'duration',
                'title' => 'Duration',
                'entity' => 'Activity',
                'description' => 'Planned or actual duration of activity expressed in minutes. Conglomerate of former duration_hours and duration_minutes.',
                'required' => false,
                'options' => false,
                'data_type' => 'Integer',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              6 => 
              array (
                'default_value' => NULL,
                'name' => 'location',
                'title' => 'Location',
                'entity' => 'Activity',
                'description' => 'Location of the activity (optional, open text).',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              7 => 
              array (
                'default_value' => NULL,
                'name' => 'phone_id',
                'title' => 'Phone (called) ID',
                'entity' => 'Activity',
                'description' => 'Phone ID of the number called (optional - used if an existing phone number is selected).',
                'required' => false,
                'options' => false,
                'data_type' => 'Integer',
                'fk_entity' => 'Phone',
                'serialize' => NULL,
              ),
              8 => 
              array (
                'default_value' => NULL,
                'name' => 'phone_number',
                'title' => 'Phone (called) Number',
                'entity' => 'Activity',
                'description' => 'Phone number in case the number does not exist in the civicrm_phone table.',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              9 => 
              array (
                'default_value' => NULL,
                'name' => 'details',
                'title' => 'Details',
                'entity' => 'Activity',
                'description' => 'Details about the activity (agenda, notes, etc).',
                'required' => false,
                'options' => false,
                'data_type' => 'Text',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              10 => 
              array (
                'default_value' => NULL,
                'name' => 'status_id',
                'title' => 'Activity Status',
                'entity' => 'Activity',
                'description' => 'ID of the status this activity is currently in. Foreign key to civicrm_option_value.',
                'required' => false,
                'options' => true,
                'data_type' => 'Integer',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              11 => 
              array (
                'default_value' => NULL,
                'name' => 'priority_id',
                'title' => 'Priority',
                'entity' => 'Activity',
                'description' => 'ID of the priority given to this activity. Foreign key to civicrm_option_value.',
                'required' => false,
                'options' => true,
                'data_type' => 'Integer',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              12 => 
              array (
                'default_value' => NULL,
                'name' => 'parent_id',
                'title' => 'Parent Activity Id',
                'entity' => 'Activity',
                'description' => 'Parent meeting ID (if this is a follow-up item). This is not currently implemented',
                'required' => false,
                'options' => false,
                'data_type' => 'Integer',
                'fk_entity' => 'Activity',
                'serialize' => NULL,
              ),
              13 => 
              array (
                'default_value' => '0',
                'name' => 'is_test',
                'title' => 'Test',
                'entity' => 'Activity',
                'description' => NULL,
                'required' => false,
                'options' => false,
                'data_type' => 'Boolean',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              14 => 
              array (
                'default_value' => 'NULL',
                'name' => 'medium_id',
                'title' => 'Activity Medium',
                'entity' => 'Activity',
                'description' => 'Activity Medium, Implicit FK to civicrm_option_value where option_group = encounter_medium.',
                'required' => false,
                'options' => true,
                'data_type' => 'Integer',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              15 => 
              array (
                'default_value' => '0',
                'name' => 'is_auto',
                'title' => 'Auto',
                'entity' => 'Activity',
                'description' => NULL,
                'required' => false,
                'options' => false,
                'data_type' => 'Boolean',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              16 => 
              array (
                'default_value' => 'NULL',
                'name' => 'relationship_id',
                'title' => 'Relationship Id',
                'entity' => 'Activity',
                'description' => 'FK to Relationship ID',
                'required' => false,
                'options' => false,
                'data_type' => 'Integer',
                'fk_entity' => 'Relationship',
                'serialize' => NULL,
              ),
              17 => 
              array (
                'default_value' => '1',
                'name' => 'is_current_revision',
                'title' => 'Is this activity a current revision in versioning chain?',
                'entity' => 'Activity',
                'description' => NULL,
                'required' => false,
                'options' => false,
                'data_type' => 'Boolean',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              18 => 
              array (
                'default_value' => NULL,
                'name' => 'original_id',
                'title' => 'Original Activity ID ',
                'entity' => 'Activity',
                'description' => 'Activity ID of the first activity record in versioning chain.',
                'required' => false,
                'options' => false,
                'data_type' => 'Integer',
                'fk_entity' => 'Activity',
                'serialize' => NULL,
              ),
              19 => 
              array (
                'default_value' => NULL,
                'name' => 'result',
                'title' => 'Result',
                'entity' => 'Activity',
                'description' => 'Currently being used to store result id for survey activity, FK to option value.',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              20 => 
              array (
                'default_value' => '0',
                'name' => 'is_deleted',
                'title' => 'Activity is in the Trash',
                'entity' => 'Activity',
                'description' => NULL,
                'required' => false,
                'options' => false,
                'data_type' => 'Boolean',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              21 => 
              array (
                'default_value' => NULL,
                'name' => 'campaign_id',
                'title' => 'Campaign',
                'entity' => 'Activity',
                'description' => 'The campaign for which this activity has been triggered.',
                'required' => false,
                'options' => true,
                'data_type' => 'Integer',
                'fk_entity' => 'Campaign',
                'serialize' => NULL,
              ),
              22 => 
              array (
                'default_value' => NULL,
                'name' => 'engagement_level',
                'title' => 'Engagement Index',
                'entity' => 'Activity',
                'description' => 'Assign a specific level of engagement to this activity. Used for tracking constituents in ladder of engagement.',
                'required' => false,
                'options' => true,
                'data_type' => 'Integer',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              23 => 
              array (
                'default_value' => NULL,
                'name' => 'weight',
                'title' => 'Order',
                'entity' => 'Activity',
                'description' => NULL,
                'required' => false,
                'options' => false,
                'data_type' => 'Integer',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              24 => 
              array (
                'default_value' => '0',
                'name' => 'is_star',
                'title' => 'Is Starred',
                'entity' => 'Activity',
                'description' => 'Activity marked as favorite.',
                'required' => false,
                'options' => false,
                'data_type' => 'Boolean',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              25 => 
              array (
                'default_value' => 'NULL',
                'name' => 'created_date',
                'title' => 'Created Date',
                'entity' => 'Activity',
                'description' => 'When was the activity was created.',
                'required' => false,
                'options' => false,
                'data_type' => 'Timestamp',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              26 => 
              array (
                'default_value' => 'CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',
                'name' => 'modified_date',
                'title' => 'Modified Date',
                'entity' => 'Activity',
                'description' => 'When was the activity (or closely related entity) was created or modified or deleted.',
                'required' => false,
                'options' => false,
                'data_type' => 'Timestamp',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
            ),
          ),
           1 => 
          array (
            'name' => 'Address',
            'description' => 'Address Entity.',
            'comment' => 'This entity holds the address informatiom of a contact. Each contact may hold
one or more addresses but must have different location types respectively.

Creating a new address requires at minimum a contact\'s ID and location type ID
and other attributes (although optional) like street address, city, country etc.',
            'fields' => 
            array (
              0 => 
              array (
                'default_value' => NULL,
                'name' => 'id',
                'title' => 'Address ID',
                'entity' => 'Address',
                'description' => 'Unique Address ID',
                'required' => true,
                'options' => false,
                'data_type' => 'Integer',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              1 => 
              array (
                'default_value' => NULL,
                'name' => 'contact_id',
                'title' => 'Contact ID',
                'entity' => 'Address',
                'description' => 'FK to Contact ID',
                'required' => false,
                'options' => false,
                'data_type' => 'Integer',
                'fk_entity' => 'Contact',
                'serialize' => NULL,
              ),
              2 => 
              array (
                'default_value' => NULL,
                'name' => 'location_type_id',
                'title' => 'Address Location Type',
                'entity' => 'Address',
                'description' => 'Which Location does this address belong to.',
                'required' => false,
                'options' => true,
                'data_type' => 'Integer',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              3 => 
              array (
                'default_value' => '0',
                'name' => 'is_primary',
                'title' => 'Is Address Primary?',
                'entity' => 'Address',
                'description' => 'Is this the primary address.',
                'required' => false,
                'options' => false,
                'data_type' => 'Boolean',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              4 => 
              array (
                'default_value' => '0',
                'name' => 'is_billing',
                'title' => 'Is Billing Address',
                'entity' => 'Address',
                'description' => 'Is this the billing address.',
                'required' => false,
                'options' => false,
                'data_type' => 'Boolean',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              5 => 
              array (
                'default_value' => NULL,
                'name' => 'street_address',
                'title' => 'Street Address',
                'entity' => 'Address',
                'description' => 'Concatenation of all routable street address components (prefix, street number, street name, suffix, unit
      number OR P.O. Box). Apps should be able to determine physical location with this data (for mapping, mail
      delivery, etc.).
    ',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              6 => 
              array (
                'default_value' => NULL,
                'name' => 'street_number',
                'title' => 'Street Number',
                'entity' => 'Address',
                'description' => 'Numeric portion of address number on the street, e.g. For 112A Main St, the street_number = 112.',
                'required' => false,
                'options' => false,
                'data_type' => 'Integer',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              7 => 
              array (
                'default_value' => NULL,
                'name' => 'street_number_suffix',
                'title' => 'Street Number Suffix',
                'entity' => 'Address',
                'description' => 'Non-numeric portion of address number on the street, e.g. For 112A Main St, the street_number_suffix = A
    ',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              8 => 
              array (
                'default_value' => NULL,
                'name' => 'street_number_predirectional',
                'title' => 'Street Direction Prefix',
                'entity' => 'Address',
                'description' => 'Directional prefix, e.g. SE Main St, SE is the prefix.',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              9 => 
              array (
                'default_value' => NULL,
                'name' => 'street_name',
                'title' => 'Street Name',
                'entity' => 'Address',
                'description' => 'Actual street name, excluding St, Dr, Rd, Ave, e.g. For 112 Main St, the street_name = Main.',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              10 => 
              array (
                'default_value' => NULL,
                'name' => 'street_type',
                'title' => 'Street Type',
                'entity' => 'Address',
                'description' => 'St, Rd, Dr, etc.',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              11 => 
              array (
                'default_value' => NULL,
                'name' => 'street_number_postdirectional',
                'title' => 'Street Direction Suffix',
                'entity' => 'Address',
                'description' => 'Directional prefix, e.g. Main St S, S is the suffix.',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              12 => 
              array (
                'default_value' => NULL,
                'name' => 'street_unit',
                'title' => 'Street Unit',
                'entity' => 'Address',
                'description' => 'Secondary unit designator, e.g. Apt 3 or Unit # 14, or Bldg 1200',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              13 => 
              array (
                'default_value' => NULL,
                'name' => 'supplemental_address_1',
                'title' => 'Supplemental Address 1',
                'entity' => 'Address',
                'description' => 'Supplemental Address Information, Line 1',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              14 => 
              array (
                'default_value' => NULL,
                'name' => 'supplemental_address_2',
                'title' => 'Supplemental Address 2',
                'entity' => 'Address',
                'description' => 'Supplemental Address Information, Line 2',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              15 => 
              array (
                'default_value' => NULL,
                'name' => 'supplemental_address_3',
                'title' => 'Supplemental Address 3',
                'entity' => 'Address',
                'description' => 'Supplemental Address Information, Line 3',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              16 => 
              array (
                'default_value' => NULL,
                'name' => 'city',
                'title' => 'City',
                'entity' => 'Address',
                'description' => 'City, Town or Village Name.',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              17 => 
              array (
                'default_value' => NULL,
                'name' => 'county_id',
                'title' => 'County',
                'entity' => 'Address',
                'description' => 'Which County does this address belong to.',
                'required' => false,
                'options' => true,
                'data_type' => 'Integer',
                'fk_entity' => 'County',
                'serialize' => NULL,
              ),
              18 => 
              array (
                'default_value' => NULL,
                'name' => 'state_province_id',
                'title' => 'State/Province',
                'entity' => 'Address',
                'description' => 'Which State_Province does this address belong to.',
                'required' => false,
                'options' => true,
                'data_type' => 'Integer',
                'fk_entity' => 'StateProvince',
                'serialize' => NULL,
              ),
              19 => 
              array (
                'default_value' => NULL,
                'name' => 'postal_code_suffix',
                'title' => 'Postal Code Suffix',
                'entity' => 'Address',
                'description' => 'Store the suffix, like the +4 part in the USPS system.',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              20 => 
              array (
                'default_value' => NULL,
                'name' => 'postal_code',
                'title' => 'Postal Code',
                'entity' => 'Address',
                'description' => 'Store both US (zip5) AND international postal codes. App is responsible for country/region appropriate validation.',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              21 => 
              array (
                'default_value' => NULL,
                'name' => 'usps_adc',
                'title' => 'USPS Code',
                'entity' => 'Address',
                'description' => 'USPS Bulk mailing code.',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              22 => 
              array (
                'default_value' => NULL,
                'name' => 'country_id',
                'title' => 'Country',
                'entity' => 'Address',
                'description' => 'Which Country does this address belong to.',
                'required' => false,
                'options' => true,
                'data_type' => 'Integer',
                'fk_entity' => 'Country',
                'serialize' => NULL,
              ),
              23 => 
              array (
                'default_value' => NULL,
                'name' => 'geo_code_1',
                'title' => 'Latitude',
                'entity' => 'Address',
                'description' => 'Latitude',
                'required' => false,
                'options' => false,
                'data_type' => 'Float',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              24 => 
              array (
                'default_value' => NULL,
                'name' => 'geo_code_2',
                'title' => 'Longitude',
                'entity' => 'Address',
                'description' => 'Longitude',
                'required' => false,
                'options' => false,
                'data_type' => 'Float',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              25 => 
              array (
                'default_value' => '0',
                'name' => 'manual_geo_code',
                'title' => 'Is manually geocoded',
                'entity' => 'Address',
                'description' => 'Is this a manually entered geo code',
                'required' => false,
                'options' => false,
                'data_type' => 'Boolean',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              26 => 
              array (
                'default_value' => NULL,
                'name' => 'timezone',
                'title' => 'Timezone',
                'entity' => 'Address',
                'description' => 'Timezone expressed as a UTC offset - e.g. United States CST would be written as "UTC-6".',
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              27 => 
              array (
                'default_value' => NULL,
                'name' => 'name',
                'title' => 'Address Name',
                'entity' => 'Address',
                'description' => NULL,
                'required' => false,
                'options' => false,
                'data_type' => 'String',
                'fk_entity' => NULL,
                'serialize' => NULL,
              ),
              28 => 
              array (
                'default_value' => NULL,
                'name' => 'master_id',
                'title' => 'Master Address Belongs To',
                'entity' => 'Address',
                'description' => 'FK to Address ID',
                'required' => false,
                'options' => false,
                'data_type' => 'Integer',
                'fk_entity' => 'Address',
                'serialize' => NULL,
              ),
            ),
          ),
        )),
      ),
    ),
    3 => 
    array (
      'file' => '/Users/emcnaughton/buildkit/build/wmff/civicrm/Civi/API/Kernel.php',
      'line' => 168,
      'function' => 'invoke',
      'class' => 'Civi\\Api4\\Provider\\ActionObjectProvider',
      'type' => '->',
      'args' => 
      array (
        0 => 
        Civi\Api4\Action\Entity\GetFields::__set_state(array(
           'checkPermissions' => true,
           'includeCustom' => true,
           'getOptions' => false,
           'fields' => 
          array (
          ),
           'select' => 
          array (
          ),
           'action' => 'get',
           'version' => 4,
           'entityName' => 'Entity',
           'actionName' => 'getFields',
           'thisReflection' => 
          ReflectionClass::__set_state(array(
             'name' => 'Civi\\Api4\\Action\\Entity\\GetFields',
          )),
           'thisParamInfo' => 
          array (
            'checkPermissions' => 
            array (
              'description' => 'Override default to allow open access',
              'comment' => 'Setting to FALSE will disable permission checks and override ACLs.
In REST/javascript this cannot be disabled.',
              'type' => 
              array (
                0 => 'bool',
              ),
              'default' => false,
            ),
            'includeCustom' => 
            array (
              'description' => 'Include custom fields for this entity, or only core fields?',
              'type' => 
              array (
                0 => 'bool',
              ),
              'default' => true,
            ),
            'getOptions' => 
            array (
              'description' => 'Fetch option lists for fields?',
              'type' => 
              array (
                0 => 'bool',
              ),
              'default' => false,
            ),
            'fields' => 
            array (
              'description' => 'Which fields should be returned?',
              'comment' => 'Ex: [\'contact_type\', \'contact_sub_type\']',
              'type' => 
              array (
                0 => 'array',
              ),
              'default' => 
              array (
              ),
            ),
            'select' => 
            array (
              'description' => 'Which attributes of the fields should be returned?',
              'options' => 
              array (
                0 => 'name',
                1 => 'title',
                2 => 'description',
                3 => 'default_value',
                4 => 'required',
                5 => 'options',
                6 => 'data_type',
                7 => 'fk_entity',
                8 => 'serialize',
                9 => 'custom_field_id',
                10 => 'custom_group_id',
              ),
              'type' => 
              array (
                0 => 'array',
              ),
              'default' => 
              array (
              ),
            ),
            'action' => 
            array (
              'type' => 
              array (
                0 => 'string',
              ),
              'default' => 'get',
            ),
          ),
           'thisArrayStorage' => 
          array (
            'wrappers' => 
            array (
              0 => 
              CRM_Utils_API_HTMLInputCoder::__set_state(array(
                 'skipFields' => NULL,
              )),
              1 => 
              CRM_Utils_API_NullOutputCoder::__set_state(array(
              )),
              2 => 
              CRM_Utils_API_ReloadOption::__set_state(array(
              )),
              3 => 
              CRM_Utils_API_MatchOption::__set_state(array(
              )),
            ),
          ),
        )),
      ),
    ),
    4 => 
    array (
      'file' => '/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/Civi/Api4/Generic/AbstractAction.php',
      'line' => 157,
      'function' => 'runRequest',
      'class' => 'Civi\\API\\Kernel',
      'type' => '->',
      'args' => 
      array (
        0 => 
        Civi\Api4\Action\Entity\GetFields::__set_state(array(
           'checkPermissions' => true,
           'includeCustom' => true,
           'getOptions' => false,
           'fields' => 
          array (
          ),
           'select' => 
          array (
          ),
           'action' => 'get',
           'version' => 4,
           'entityName' => 'Entity',
           'actionName' => 'getFields',
           'thisReflection' => 
          ReflectionClass::__set_state(array(
             'name' => 'Civi\\Api4\\Action\\Entity\\GetFields',
          )),
           'thisParamInfo' => 
          array (
            'checkPermissions' => 
            array (
              'description' => 'Override default to allow open access',
              'comment' => 'Setting to FALSE will disable permission checks and override ACLs.
In REST/javascript this cannot be disabled.',
              'type' => 
              array (
                0 => 'bool',
              ),
              'default' => false,
            ),
            'includeCustom' => 
            array (
              'description' => 'Include custom fields for this entity, or only core fields?',
              'type' => 
              array (
                0 => 'bool',
              ),
              'default' => true,
            ),
            'getOptions' => 
            array (
              'description' => 'Fetch option lists for fields?',
              'type' => 
              array (
                0 => 'bool',
              ),
              'default' => false,
            ),
            'fields' => 
            array (
              'description' => 'Which fields should be returned?',
              'comment' => 'Ex: [\'contact_type\', \'contact_sub_type\']',
              'type' => 
              array (
                0 => 'array',
              ),
              'default' => 
              array (
              ),
            ),
            'select' => 
            array (
              'description' => 'Which attributes of the fields should be returned?',
              'options' => 
              array (
                0 => 'name',
                1 => 'title',
                2 => 'description',
                3 => 'default_value',
                4 => 'required',
                5 => 'options',
                6 => 'data_type',
                7 => 'fk_entity',
                8 => 'serialize',
                9 => 'custom_field_id',
                10 => 'custom_group_id',
              ),
              'type' => 
              array (
                0 => 'array',
              ),
              'default' => 
              array (
              ),
            ),
            'action' => 
            array (
              'type' => 
              array (
                0 => 'string',
              ),
              'default' => 'get',
            ),
          ),
           'thisArrayStorage' => 
          array (
            'wrappers' => 
            array (
              0 => 
              CRM_Utils_API_HTMLInputCoder::__set_state(array(
                 'skipFields' => NULL,
              )),
              1 => 
              CRM_Utils_API_NullOutputCoder::__set_state(array(
              )),
              2 => 
              CRM_Utils_API_ReloadOption::__set_state(array(
              )),
              3 => 
              CRM_Utils_API_MatchOption::__set_state(array(
              )),
            ),
          ),
        )),
      ),
    ),
    5 => 
    array (
      'file' => '/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/api4.php',
      'line' => 36,
      'function' => 'execute',
      'class' => 'Civi\\Api4\\Generic\\AbstractAction',
      'type' => '->',
      'args' => 
      array (
      ),
    ),
    6 => 
    array (
      'file' => '/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/CRM/Api4/Page/AJAX.php',
      'line' => 55,
      'function' => 'civicrm_api4',
      'args' => 
      array (
        0 => 'Entity',
        1 => 'getFields',
        2 => 
        array (
          'checkPermissions' => true,
        ),
      ),
    ),
    7 => 
    array (
      'file' => '/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/CRM/Api4/Page/AJAX.php',
      'line' => 16,
      'function' => 'execute',
      'class' => 'CRM_Api4_Page_AJAX',
      'type' => '->',
      'args' => 
      array (
        0 => 'Entity',
        1 => 'getFields',
        2 => 
        array (
          'checkPermissions' => true,
        ),
      ),
    ),
    8 => 
    array (
      'file' => '/Users/emcnaughton/buildkit/build/wmff/civicrm/CRM/Core/Invoke.php',
      'line' => 311,
      'function' => 'run',
      'class' => 'CRM_Api4_Page_AJAX',
      'type' => '->',
      'args' => 
      array (
        0 => 
        array (
          0 => 'civicrm',
          1 => 'ajax',
          2 => 'api4',
        ),
        1 => NULL,
      ),
    ),
    9 => 
    array (
      'file' => '/Users/emcnaughton/buildkit/build/wmff/civicrm/CRM/Core/Invoke.php',
      'line' => 85,
      'function' => 'runItem',
      'class' => 'CRM_Core_Invoke',
      'type' => '::',
      'args' => 
      array (
        0 => 
        array (
          'id' => '3603',
          'domain_id' => '1',
          'path' => 'civicrm/ajax/api4',
          'title' => 'Api v4',
          'access_callback' => 
          array (
            0 => 'CRM_Core_Permission',
            1 => 'checkMenu',
          ),
          'access_arguments' => 
          array (
            0 => 
            array (
              0 => 'access CiviCRM',
            ),
            1 => 'and',
          ),
          'page_callback' => 'CRM_Api4_Page_AJAX',
          'breadcrumb' => 
          array (
            0 => 
            array (
              'title' => 'CiviCRM',
              'url' => '/civicrm?reset=1',
            ),
          ),
          'is_ssl' => '0',
          'weight' => '1',
          'type' => '1',
          'page_type' => '0',
          'page_arguments' => false,
        ),
      ),
    ),
    10 => 
    array (
      'file' => '/Users/emcnaughton/buildkit/build/wmff/civicrm/CRM/Core/Invoke.php',
      'line' => 52,
      'function' => '_invoke',
      'class' => 'CRM_Core_Invoke',
      'type' => '::',
      'args' => 
      array (
        0 => 
        array (
          0 => 'civicrm',
          1 => 'ajax',
          2 => 'api4',
        ),
      ),
    ),
    11 => 
    array (
      'file' => '/Users/emcnaughton/buildkit/build/wmff/civicrm/drupal/civicrm.module',
      'line' => 445,
      'function' => 'invoke',
      'class' => 'CRM_Core_Invoke',
      'type' => '::',
      'args' => 
      array (
        0 => 
        array (
          0 => 'civicrm',
          1 => 'ajax',
          2 => 'api4',
        ),
      ),
    ),
    12 => 
    array (
      'file' => '/Users/emcnaughton/buildkit/build/wmff/drupal/includes/menu.inc',
      'line' => 527,
      'function' => 'civicrm_invoke',
      'args' => 
      array (
        0 => 'ajax',
        1 => 'api4',
      ),
    ),
    13 => 
    array (
      'file' => '/Users/emcnaughton/buildkit/build/wmff/drupal/index.php',
      'line' => 21,
      'function' => 'menu_execute_active_handler',
      'args' => 
      array (
      ),
    ),
  ),
   'previous' => NULL,
   'xdebug_message' => '<tr><th align=\'left\' bgcolor=\'#f57900\' colspan="5"><span style=\'background-color: #cc0000; color: #fce94f; font-size: x-large;\'>( ! )</span> Civi\\API\\Exception\\NotImplementedException: Api Afform getFields version 4 does not exist. in /Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/Civi/Api4/Generic/AbstractEntity.php on line <i>53</i></th></tr>
<tr><th align=\'left\' bgcolor=\'#e9b96e\' colspan=\'5\'>Call Stack</th></tr>
<tr><th align=\'center\' bgcolor=\'#eeeeec\'>#</th><th align=\'left\' bgcolor=\'#eeeeec\'>Time</th><th align=\'left\' bgcolor=\'#eeeeec\'>Memory</th><th align=\'left\' bgcolor=\'#eeeeec\'>Function</th><th align=\'left\' bgcolor=\'#eeeeec\'>Location</th></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>1</td><td bgcolor=\'#eeeeec\' align=\'center\'>0.0074</td><td bgcolor=\'#eeeeec\' align=\'right\'>409624</td><td bgcolor=\'#eeeeec\'>{main}(  )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/drupal/index.php\' bgcolor=\'#eeeeec\'>.../index.php<b>:</b>0</td></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>2</td><td bgcolor=\'#eeeeec\' align=\'center\'>0.4410</td><td bgcolor=\'#eeeeec\' align=\'right\'>17782160</td><td bgcolor=\'#eeeeec\'>menu_execute_active_handler( ???, ??? )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/drupal/index.php\' bgcolor=\'#eeeeec\'>.../index.php<b>:</b>21</td></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>3</td><td bgcolor=\'#eeeeec\' align=\'center\'>0.4412</td><td bgcolor=\'#eeeeec\' align=\'right\'>17782664</td><td bgcolor=\'#eeeeec\'>civicrm_invoke( ???, ??? )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/drupal/includes/menu.inc\' bgcolor=\'#eeeeec\'>.../menu.inc<b>:</b>527</td></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>4</td><td bgcolor=\'#eeeeec\' align=\'center\'>1.1714</td><td bgcolor=\'#eeeeec\' align=\'right\'>28632576</td><td bgcolor=\'#eeeeec\'>CRM_Core_Invoke::invoke( ??? )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/civicrm/drupal/civicrm.module\' bgcolor=\'#eeeeec\'>.../civicrm.module<b>:</b>445</td></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>5</td><td bgcolor=\'#eeeeec\' align=\'center\'>1.1714</td><td bgcolor=\'#eeeeec\' align=\'right\'>28632576</td><td bgcolor=\'#eeeeec\'>CRM_Core_Invoke::_invoke( ??? )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/civicrm/CRM/Core/Invoke.php\' bgcolor=\'#eeeeec\'>.../Invoke.php<b>:</b>52</td></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>6</td><td bgcolor=\'#eeeeec\' align=\'center\'>1.1851</td><td bgcolor=\'#eeeeec\' align=\'right\'>28811072</td><td bgcolor=\'#eeeeec\'>CRM_Core_Invoke::runItem( ??? )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/civicrm/CRM/Core/Invoke.php\' bgcolor=\'#eeeeec\'>.../Invoke.php<b>:</b>85</td></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>7</td><td bgcolor=\'#eeeeec\' align=\'center\'>1.2142</td><td bgcolor=\'#eeeeec\' align=\'right\'>29381488</td><td bgcolor=\'#eeeeec\'>CRM_Api4_Page_AJAX->run( ???, ??? )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/civicrm/CRM/Core/Invoke.php\' bgcolor=\'#eeeeec\'>.../Invoke.php<b>:</b>311</td></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>8</td><td bgcolor=\'#eeeeec\' align=\'center\'>6.0903</td><td bgcolor=\'#eeeeec\' align=\'right\'>29387232</td><td bgcolor=\'#eeeeec\'>CRM_Api4_Page_AJAX->execute( ???, ???, ??? )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/CRM/Api4/Page/AJAX.php\' bgcolor=\'#eeeeec\'>.../AJAX.php<b>:</b>16</td></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>9</td><td bgcolor=\'#eeeeec\' align=\'center\'>6.0903</td><td bgcolor=\'#eeeeec\' align=\'right\'>29387608</td><td bgcolor=\'#eeeeec\'>civicrm_api4( ???, ???, ??? )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/CRM/Api4/Page/AJAX.php\' bgcolor=\'#eeeeec\'>.../AJAX.php<b>:</b>55</td></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>10</td><td bgcolor=\'#eeeeec\' align=\'center\'>6.1373</td><td bgcolor=\'#eeeeec\' align=\'right\'>29459376</td><td bgcolor=\'#eeeeec\'>Civi\\Api4\\Action\\Entity\\GetFields->execute(  )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/api4.php\' bgcolor=\'#eeeeec\'>.../api4.php<b>:</b>36</td></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>11</td><td bgcolor=\'#eeeeec\' align=\'center\'>6.1598</td><td bgcolor=\'#eeeeec\' align=\'right\'>29802280</td><td bgcolor=\'#eeeeec\'>Civi\\API\\Kernel->runRequest( ??? )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/Civi/Api4/Generic/AbstractAction.php\' bgcolor=\'#eeeeec\'>.../AbstractAction.php<b>:</b>157</td></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>12</td><td bgcolor=\'#eeeeec\' align=\'center\'>6.1771</td><td bgcolor=\'#eeeeec\' align=\'right\'>29891880</td><td bgcolor=\'#eeeeec\'>Civi\\Api4\\Provider\\ActionObjectProvider->invoke( ??? )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/civicrm/Civi/API/Kernel.php\' bgcolor=\'#eeeeec\'>.../Kernel.php<b>:</b>168</td></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>13</td><td bgcolor=\'#eeeeec\' align=\'center\'>6.1798</td><td bgcolor=\'#eeeeec\' align=\'right\'>29899128</td><td bgcolor=\'#eeeeec\'>Civi\\Api4\\Action\\Entity\\GetFields->_run( ??? )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/Civi/Api4/Provider/ActionObjectProvider.php\' bgcolor=\'#eeeeec\'>.../ActionObjectProvider.php<b>:</b>79</td></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>14</td><td bgcolor=\'#eeeeec\' align=\'center\'>9.7155</td><td bgcolor=\'#eeeeec\' align=\'right\'>42435416</td><td bgcolor=\'#eeeeec\'>civicrm_api4( ???, ???, ??? )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/Civi/Api4/Action/Entity/GetFields.php\' bgcolor=\'#eeeeec\'>.../GetFields.php<b>:</b>20</td></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>15</td><td bgcolor=\'#eeeeec\' align=\'center\'>9.7155</td><td bgcolor=\'#eeeeec\' align=\'right\'>42435840</td><td bgcolor=\'#eeeeec\'>Civi\\Api4\\Generic\\AbstractEntity::getFields(  )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/api4.php\' bgcolor=\'#eeeeec\'>.../api4.php<b>:</b>30</td></tr>
<tr><td bgcolor=\'#eeeeec\' align=\'center\'>16</td><td bgcolor=\'#eeeeec\' align=\'center\'>9.7155</td><td bgcolor=\'#eeeeec\' align=\'right\'>42435896</td><td bgcolor=\'#eeeeec\'>Civi\\Api4\\Generic\\AbstractEntity::__callStatic( ???, ??? )</td><td title=\'/Users/emcnaughton/buildkit/build/wmff/civicrm/ext/api4/api4.php\' bgcolor=\'#eeeeec\'>.../api4.php<b>:</b>30</td></tr>
',
))

How decoupled is this architecture?

I'm starting to think outside of the Drupal box, and I'm wanting to understand what's going on here. Is it possible that Mark moves past Drupal-only thinking, IDK ....Decouple so to speak?

I'm thinking about how to make CiviCRM form functionality be completely decoupled from the CMS, or even the PHP based CiviCRM system as it is now.

The documentation reads to me like you still need a CiviCRM extension, and these forms will require the CiviCRM PHP system ... but maybe I'm missing something. You have nice REST API, with new and better on the way .. so technically it's already possible to decouple CiviCRM. What's missing is some standardization where all a content editor needs to do is add a tag

It seems to me that it could be possible to have a JS infrastructure where you can import the necessary JS, have your html templates, and drop that anywhere, and have the same form, with ajax to do the API requests to make a contribution, register for an event etc...

I'm thinking about functional units that could be placed in a static html website, Wordpress, Joomla, Drupal, served up by NodeJS, or even copy/pasted into something like Wix...Could this get to a place where no matter how you render the html and load the javascript, you get standardized CiviCRM functionality? How decoupled can this get?

I don't much about these javascript frameworks, there are many of them, but I'm willing to learn now and be open to new ideas, and I'm seeing that it's possible to some very amazing things...

This is the video on webcomponents really kicked down the walls in my mind: https://www.youtube.com/watch?v=Akn6keIYZ3k

https://www.webcomponents.org/

Same guy is building something called HAX (Headless Authoring Experience) https://haxtheweb.org/
Which works like that, you can drop it into Drupal, Wordpress, Joomla, static html, whatever...

Webcomponents are JS library agnostic, in the sense that they could be used with (or without) Angular or React or whatever. So as far as my limited understand goes, you can use webcomponents with AngularJS, it's not an either/or proposition...Theoretically, could could make webcomponents that are actually lil mini Angular apps.

I'm not trying to push any ideas for this project, because I'm not going to pretend I understand the cutting edge JS world. It's just this webcomponents standard has really opened me up to a different kind of thinking.

Does Afform have similarities / differences to webcomponents?

So just how decoupled is this Afform architecture? How far could it go?

I'm just thinking that there could be standard CiviCRM webcomponents or "whatever" where any framework, any CMS, could serve up standardized civicrm contribution / event registration / profile / any forms.

You guys are way out in the frontier here, and you're busy and don't need to explain it to me, but if you feel like it, share your thoughts with a reforming Drupal purist exploring what is to me a brave new JS world.

Anyway, I'm thinking about new things, and going to try this stuff out here, and see what it can be combined with and how it can be stretched and pulled, and of course fit into Drupal in new and unique ways :).

Is there an easy way to make afformlet crm-editable?

I have a very minor form - basic contact name details which I would like to make crm-editable. I'm wondering if there is an easy way / helper. I guess maybe I don't truly mean crm-editable as I'm undecided about instant edit vs edit + click a button

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.