2
votes

I'm considering using CakePHP (2.x) for a new project I'm working on. The project would have users that have clients. There would be default fields on the client model like name, address, email, etc.. but my client would like users to be able to add custom fields to their clients.

So User1 might want to add a "favorite color" field to their clients and User2 might want to add a "favorite sport" field to their clients. (These are just examples)

Is there a way to do this with CakePHP? What is the best solution for adding fields to models after their controllers, models, and views have already been baked?

My first thought was to set up the following models

User ( id, username, password ) hasMany: Clients

Client ( id, first_name, last_name, user_id ) belongsTo: User hasMany:ClientFields

ClientFieldKey ( id, field_name, user_id ) belongsTo: User hasMany:ClientFieldValues

ClientFieldValue ( id, client_field_key_id, field_value ) belongsTo: ClientFieldKey

ClientField ( id, client_id, client_field_value_id ) belongsTo: Client, ClientFieldValue

then I could have something like the following

Client ( id: 1, username: user1, password: pass1, user_id: 1)

Client ( id: 2, username: user2, password: pass2, user_id: 1)

Client ( id: 3, username: user3, password: pass3, user_id: 2)

Client ( id: 4, username: user4, password: pass4, user_id: 2)

ClientFieldKey ( id: 1, field_name: Color, user_id: 1 )

ClientFieldKey ( id: 1, field_name: Sport, user_id: 2 )

ClientFieldValue ( id: 1, client_field_key_id: 1, field_value: red )

ClientFieldValue ( id: 2, client_field_key_id: 1, field_value: blue )

ClientFieldValue ( id: 3, client_field_key_id: 2, field_value: hockey )

ClientFieldValue ( id: 4, client_field_key_id: 2, field_value: football )

ClientField ( id: 1, client_id: 1, client_field_value_id: 2 )

ClientField ( id: 2, client_id: 2, client_field_value_id: 1 )

ClientField ( id: 3, client_id: 3, client_field_value_id: 3 )

ClientField ( id: 4, client_id: 4, client_field_value_id: 4 )

Which would mean that user 1 created client 1 who likes blue and client 2 who likes red. User 2 created client 3 who likes hockey and client 4 likes football.

Would a model structure like this work in CakePHP? If so, how would I add the fields to the client add and edit forms dynamically and what should I call the tables?

2
Expandable Behaviour looks interesting, or KeyValue BehaviorRoss
As Ross mentioned above - Expandable is the way. Here is Expandable Plugin ready to work with 2.x with almost zero configuration.redd
@redd the Expandable Plugin looks good but it doesn't quite do what I need. It allows me to add fields on a per client basis but what I want is to add the fields to all of the clients that belong to a specific user. Can you think of anyway to do that by modifying this plugin?Devin Crossman
That's a little different from the original question. How much normalization do you need, wouldn't client fields and client field values be enough, where Client has many ClientField and ClientField has many ClientFieldValue, assuming that a client is exclusive to a user? And what exactly do you mean with "add the fields to the client add and edit forms dynamically"? Do you want to be able to add new fields dynamically in the client add/edit forms (ie adding inputs to a form via JavaScript), or are you talking about generating forms in the view using the existing client fields?ndm
@ndm a client is exclusive to the user but all of that user's clients should have the same fields. That is why I think I need to normalize the keys into their own table/model so that the user can add keys for all their clients in one place. The add/edit forms (and other views) should be able to lookup what keys the user has added to their clients and make the appropriate inputs (doesn't have to be with javascript). If a user later decides to add a field to their clients all existing clients should be updated and all new clients need to be created with that field. I hope that makes sense.Devin Crossman

2 Answers

3
votes

Update I'm not sure whether you updated your question, or whether I simply didn't read it carefully enough, however I've updated my answer so that it's about clients instead of users. /Update

I would do this using an additional table/model that defines the properties (as key/value pairs presumably), and connect it to the Client using an hasMany relation, ie Client hasMany Property.

That way you can later assign as many and whatever properties to the Client you like.

Here's an (untested) example:

The properties table

properties
+------------------------------+
| id | client_id | key | value |
+------------------------------+

The properties model

class Property extends AppModel
{
    ...

    public $belongsTo = array
    (
        'Client' => array
        (
            'className' => 'Client',
            'foreignKey' => 'client_id'
        )
    );

    ...
}

The client model

class Client extends AppModel
{
    ...

    public $hasMany = array
    (
        'Property' => array
        (
            'className'  => 'Property',
            'foreignKey' => 'client_id',
            'dependent'  => true
        )
    );

    ...
}

Adding properties to a client

$data = array
(
    'Client' => array
    (
        'id' => 1234
    ),
    'Property' = array
    (
        array
        (
            'key' => 'favorite_color',
            'value' => 'green'
        ),
        array
        (
            'key' => 'favorite_sport',
            'value' => 'hockey'
        )
    )
);

$Client->saveAssociated($data);
0
votes

I actually figured this out.. not sure if it's the best way but it works pretty well.

I created the following models:

  • User
  • Client
  • ClientFieldKey
  • ClientFieldValue

With the following relationships

User

hasMany: Client, ClientFieldKey

Client

hasMany: ClientFieldValue

belongsTo: User

ClientFieldKey

hasMany: ClientFieldValue

belongsTo: User

ClientFieldValue

belongsTo: Client, ClientFieldKey

Then the user could create the keys for the fields and in the ClientFieldKey afterSave() callback I create a new ClientFieldValue for every one of the users clients whenever a ClientFieldKey is created.

// /Model/ClientFieldKey.php
public function afterSave($created) {
    if($created):
      $clientFieldValue = ClassRegistry::init('ClientFieldValue');
      $users_clients = $this->find('all', array(
        'conditions'=>'ClientFieldKey.id = '.$this->data['ClientFieldKey']['id'], 
        'contain'=>array('User'=>array('Client'=>array('fields'=>'id')))
      ));
      foreach($users_clients[0]['User']['Client'] as $users_client):
        $clientFieldValue->create();
        $clientFieldValue->set('client_field_key_id', $this->data['ClientFieldKey']['id']);
        $clientFieldValue->set('client_id', $users_client['id']);
        $clientFieldValue->save();
      endforeach;
  endif;
}

and then set up the add/edit/view actions on my client

// /Controller/ClientsController.php
public function add() {
      ...
  $this->Client->saveAssociated($this->request->data);
      ...   
  $clientFieldKeys = $this->Client->User->find('all', array('contain' => array('ClientFieldKeys' => array('fields'=>'id,key')), 'conditions' => 'User.id = '.$this->Auth->User('id')));
  $this->set('clientFieldKeys', $clientFieldKeys[0]['ClientFieldKeys']);
}

public function edit($id = null) {
      ...
  $this->Client->saveAll($this->request->data);
      ...
  $options = array('conditions' => array('Client.' . $this->Client->primaryKey => $id), 'contain'=>array('ClientFieldValue'=>array('ClientFieldKey')));
  $this->request->data = $this->Client->find('first', $options);
}

public function view($id = null) {
      ...
  $client = $this->Client->find('first', array(
    'conditions' => array('Client.' . $this->Client->primaryKey => $id),
    'contain' => array('User', 'ClientFieldValue' => array('ClientFieldKey'))
  ));
  $this->set('client', $client);
}

// /View/Clients/add.ctp
<?php if($clientFieldKeys): ?>
  <h2>Custom Fields</h2>
  <?php
    $count=0;
    foreach($clientFieldKeys as $cfk):
      echo $this->Form->hidden('ClientFieldValue.'.$count.'.client_field_key_id', array('value'=>$cfk['id']));
      echo $this->Form->input('ClientFieldValue.'.$count.'.value', array('type'=>'text', 'label'=>$cfk['key']));
      $count++;
    endforeach;
  ?>
<?php endif; ?>

// /View/Clients/edit.ctp
<?php if($this->data['ClientFieldValue']): ?>
  <h2>Custom Fields</h2>
  <?php
    $count=0;
    foreach($this->data['ClientFieldValue'] as $cfv):
      echo $this->Form->hidden('ClientFieldValue.'.$count.'.id');
      echo $this->Form->input('ClientFieldValue.'.$count.'.value', array('type'=>'text','label'=>$cfv['ClientFieldKey']['key']));
      $count++;
    endforeach;
  ?>
<?php endif; ?>

// /View/Clients/view.ctp
<?php if($client['ClientFieldValue']): ?>
  <h2>Custom Fields</h2>
  <?php foreach($client['ClientFieldValue'] as $clientFieldValue): ?>
    <dt><?php echo $clientFieldValue['ClientFieldKey']['key']; ?></dt>
      <dd>
        <?php echo $clientFieldValue['value']; ?>
        &nbsp;
      </dd>
  <?php endforeach; ?>
<?php endif; ?>