CiviCRM Community Forums (archive)

*

News:

Have a question about CiviCRM?
Get it answered quickly at the new
CiviCRM Stack Exchange Q+A site

This forum was archived on 25 November 2017. Learn more.
How to get involved.
What to do if you think you've found a bug.



  • CiviCRM Community Forums (archive) »
  • Old sections (read-only, deprecated) »
  • Developer Discussion »
  • 5.0 Saloon »
  • Doctrine and an API Kernel
Pages: [1] 2

Author Topic: Doctrine and an API Kernel  (Read 3542 times)

totten

  • Administrator
  • Ask me questions
  • *****
  • Posts: 695
  • Karma: 64
Doctrine and an API Kernel
March 24, 2014, 07:04:58 pm

In our work with Doctrine, some basic questions have arisen: how would a developer use it? And how would we phase out DAO?

For PHP developers writing server-side code, the answer can be straight-forward: start writing code that uses Doctrine directly! Eventually rewrite or deprecate code that relies on DAO.  This has an upside because Doctrine provides much richer interface than DAO.

For developers writing remote code (such as rich Javascript apps), one doesn't call DAO directly; rather, one uses a number of APIs which have been built on DAO/BAO.  Phasing out DAO would mean reimplemeting or replacing these API's.  When tackling this problem, we should continue a trend in our API's evolution: shifting away from complex, adhoc API implementations towards thinner APIs driven by metadata.

One approach -- which I experimented heavily with -- would be to fully replace our DAO+API stack with Doctrine and a REST library (like http://leedavis81.github.io/drest/ , https://github.com/FriendsOfSymfony/FOSRestBundle, or http://hateoas-php.org/ ).  This sounds promising at first because it uses more third-party code (and reduces the amount of inhouse code), but fundamentally these are libraries -- not a fully-formed API.  There's a lot of valuable, generic services in our current API layer -- things like permissioning, authentication, chaining, option-values, and Drush+Smarty bindings -- which would need to be reimplemented or reinvented in some similar-but-different way to use these libraries.  Crucially, reimplementing the API's generic services would fail two goals: it would significantly change API DX and it would still leave us with a new chunk of code to maintain.  I think it would be OK to compromise on one of these for 5.0, but I'm skeptical of compromising both.

The next approach is to keep our basic API DX and runtime but allow incremental progress:

  • Use the same basic API layer for Doctrine- and DAO-based APIs -- civicrm_api(), CRM.api(), {crmAPI}, etc.
  • API requests with "version=>3" use DAOs; with "version=>4", use Doctrine.
  • Initially provide very thin, generic Doctrine-based APIs for a long list of entities.
  • Use Doctrine metadata (annotations) and classes to drive APIv4 (instead of XML + GenCode + procedural magic-functions).
  • As the Doctrine-based APIv4 matures, deprecate the DAO-based APIv3.
  • As the Doctrine-based APIv4 matures, introduces more services/interfaces which take advantage of Doctrine.

This incremental approach requires some changes to implementation of civicrm_api() and its various helpers -- which leads to the final topic: making the the API-layer cleaner and more flexible. I've developed a patch here:

  • Overall patch: https://github.com/totten/civicrm-core/compare/civicrm:doctrine...doctrine-api-kernel?expand=1
  • Old civicrm_api(): https://github.com/civicrm/civicrm-core/blob/0ce8483729744eef1649ad91f908fda25ee5223f/api/api.php#L13
  • New Civi\API\Kernel::run(): https://github.com/totten/civicrm-core/blob/360207acabb39d2c211934978ccfd29b089ea352/Civi/API/Kernel.php#L69

A few things to notice:

  • The original civicrm_api() has three different calls to $transaction->rollback for different call-paths. The new kernel doesn't need to know anything about how transactions are implemented. Instead, that's handled by https://github.com/totten/civicrm-core/blob/360207acabb39d2c211934978ccfd29b089ea352/Civi/API/Subscriber/TransactionSubscriber.php -- and there's only one call to rollback().
  • The original civicrm_api() looks up a callback function using our magic-function-naming convention (line 49), and -- when executing the callback -- the function signature is adapted depending on how the particular callback was designed (line 77-91). In the refactored version, the magic-function-naming convention has been isolated in its own class ( https://github.com/totten/civicrm-core/blob/360207acabb39d2c211934978ccfd29b089ea352/Civi/API/Provider/MagicFunctionProvider.php#L37 ). We can implement other API providers which work differently (eg generic providers based on Doctrine, the custom-data system, or third-party services).
  • To get a high-level summary of features supported by the kernel, look at https://github.com/totten/civicrm-core/blob/360207acabb39d2c211934978ccfd29b089ea352/Civi/Core/Container.php#L192 . Features can be added and removed by adding/removing event subscribers.
  • I've been frequently running the api_v3_SyntaxConformanceTest (and spot-checking some other tests); so far, this has avoided several regressions.
  • You might expect that adding several new classes and event listeners would impact performance, but some quick performance testing on my laptop suggests that the changes have little affect. (See data the bottom)

The work isn't done yet -- my next step is writing a DoctrineCrudProvider which exposes all Doctrine entities via the API (as long as they have the annotation @CiviAPI\Entity).

---------------
Performance Without Event-Driven Kernel
 * Running SyntaxConformanceTest takes ~139sec. (Three trials: 139sec, 138sec, 139sec). ((NOTE: This is a good test of "running a huge number of API calls within one request".))
 * Running "drush cvapi contact.get" 50 times takes 59 sec. ((NOTE: This is a good test of "running a small number of API calls but bootstrapping frequently".)

Performance With Event-Driven Kernel:
 * Running SyntaxConformanceTest takes ~139sec (Three trials: 139sec, 142sec, 139sec)
 * Running "drush cvapi contact.get" 50 times takes 59 sec.

Eileen

  • Forum Godess / God
  • I’m (like) Lobo ;)
  • *****
  • Posts: 4195
  • Karma: 218
    • Fuzion
Re: Doctrine and an API Kernel
March 24, 2014, 07:19:51 pm
Looks very exciting - get seems like the biggest challenge to me as we have several patterns

1) DAO based - e.g. address, email, website
1.1) DAO based + extras .e.g event adds 'isCurrent'
2) Query object based - api_query call ie.. contact
2.1) Query object based - use query object to generate sql
2.2) Query object based + extras - e.g. contribution does some funky stuff around soft credits
3) Multiple somewhat random efforts - e.g case, activitiy, entity tag

So, I understand & am keen to see the demise of the random ones & am happy to drop the extra handlings out of the apiv4 & introduce them as necessary. How do you see the DAO vs Query object working? Do we stick with the query object for contact, contribution, pledge, participant? If we look at advanced search there are 7 entities that can be rendered from advanced search - which arguably should be treated the same from this api.get point of view -
contact, participant, activity, contribution, membership, case, mailing recipients & (ahem) related contacts.

It should be noted that the use of uniquenames in the api has been about supporting these query objects & as a result we now 'encourage' the use of unique names on all get calls - but the right approach WRT uniquenames is still not clear to me.
Make today the day you step up to support CiviCRM and all the amazing organisations that are using it to improve our world - http://civicrm.org/contribute

nicolas

  • I post occasionally
  • **
  • Posts: 92
  • Karma: 6
    • cividesk
  • CiviCRM version: 4.4 LTS
  • CMS version: Standalone (yep)
  • MySQL version: 5.1
  • PHP version: 5.3
Re: Doctrine and an API Kernel
March 24, 2014, 07:23:08 pm
Tim - I must say I like this approach as it moves the API 'intelligence' down to the Doctrine layer as much as possible - and therefore all core code also benefits from it. The API then becomes a 'thin translation layer' between our various external interfaces and the Doctrine core/annotations/classes. This is appealing from an architectural point of view as it improves consistency and minimizes reimplementation between core and API.
cividesk -- CiviCRM delivered ... your way!

totten

  • Administrator
  • Ask me questions
  • *****
  • Posts: 695
  • Karma: 64
Re: Doctrine and an API Kernel
March 24, 2014, 08:32:25 pm
That's a pretty good breakdown of existing API groups. You might add an item for pseudo-entities ("Setting", "Job", "System") which aren't in the same bucket as "Case", "Activity", or "Entity Tag".

For the foreseeable future, I think all the APIv3 implementations remain the same, and -- as eileen says -- we enhance APIv4 as necessary. The improvements to APIv4 would (hopefully, eventually) satisfy the same requirements/use-cases as APIv3 (but not as strict drop-in replacements).

I haven't thought too hard about how exactly we would handle the requirements/use-cases addressed by Query-object-APIs -- they allow some pretty sophisticated querying. Two under-formed ideas: either build on top of Doctrine's Query Builder ( http://docs.doctrine-project.org/en/latest/reference/query-builder.html ) or allow DQL ( http://docs.doctrine-project.org/en/latest/reference/dql-doctrine-query-language.html ) with automatic filtering (for permissions/ACLs). Allowing permissioned DQL is arguably the most flexible, and it allows re-use of an existing language/format rather than making a new one, and some competitors take a similar tact (eg Salesforce's REST API accepts SOQL). However, from a security perspective, permissioned DQL poses the most challenge+risk, and the DQL approach is "all or nothing". Writing something based on Doctrine's Query Builder allows more cautious, incremental implementation.

Eileen

  • Forum Godess / God
  • I’m (like) Lobo ;)
  • *****
  • Posts: 4195
  • Karma: 218
    • Fuzion
Re: Doctrine and an API Kernel
March 24, 2014, 09:04:41 pm
So, putting aside 'odds & sods' - I think the existing query & dao get types are the ones we need to plan ahead for. I don't feel like other crud items have any systematic differences (ie. some of them are a bit odd but there is no fundamental reason for them to be unstandardised).

Putting it another way - the two methods do one of 2 things
1) expose advanced search / search builder functionality to the api
2) expose fairly basic systematic access to the api

So, I guess the first question is - are these going to converge or remain as 2 strands. Note that the search builder functionality goes quite far beyond anything systematic in some cases - e.g the relationship based critieria involves constructing temp tables to deal with the potentially unknown relationship direction.

The basic strand is somewhat inadequate currently - e.g. address api will not apply contact based acls.
Make today the day you step up to support CiviCRM and all the amazing organisations that are using it to improve our world - http://civicrm.org/contribute

totten

  • Administrator
  • Ask me questions
  • *****
  • Posts: 695
  • Karma: 64
Re: Doctrine and an API Kernel
March 24, 2014, 09:31:51 pm
Agree that the permissions on current APIs are inadequate (like Address API + ACLs), and it would be great to address that systematically.

I think that reaching parity with the generic DAO-based filtering will be pretty straightforward using QueryBuilder and Criteria. (One of the Doctrine<->REST attempts gets about half-way there with only a few lines of code -- https://github.com/civicrm/civicrm-core/blob/doctrine/Civi/API/Page/REST.php#L106 . It supports equality filters; adding filters for <, >, IN, etc should be straightforward.)

The sticky part is the query-object / advanced search, but -- whatever design we come up with there -- I don't think it will affect the approach for the simpler entities, so it seems reasonable to start by supporting the simpler entities.

xavier

  • Forum Godess / God
  • I’m (like) Lobo ;)
  • *****
  • Posts: 4453
  • Karma: 161
    • Tech To The People
  • CiviCRM version: yes probably
  • CMS version: drupal
Re: Doctrine and an API Kernel
March 24, 2014, 11:08:26 pm
What happen to the BAO in this picture? Some actions replies on it (eg the create one).
Some API users relies on some of the BAO features (eg triggering the hook pre/post).

Can doctrine cover both the DAO and the BAO (eg. with the annotations), are we going to migrate BAO to use doctrine?  Do you see any easy way out?

X+
-Hackathon and data journalism about the European parliament 24-26 jan. Watch out the result

totten

  • Administrator
  • Ask me questions
  • *****
  • Posts: 695
  • Karma: 64
Re: Doctrine and an API Kernel
March 25, 2014, 12:03:08 am
Re: actions/triggers -- Doctrine has its own system of event-listeners ( http://docs.doctrine-project.org/en/latest/reference/events.html ) which is comparable to the pre/post hooks. We'll need to provide a way to register event-listeners or provide a bridge between Doctrine events and CMS hooks, but either case shouldn't be hard.

More generally, regarding migration of BAO logic... I don't see an easy way out, but we've got to start somewhere. Starting with generic CRUD is appealing because (a) we know we'll need it for a certain range of entities, (b) the original Civi API neglected generic CRUD early on which ultimately made the code-base more difficult to work with, (c) it gives us something we can experiment with/build on for all entities, and finally (d) even if a particular entity winds up with some specialized (non-generic) implementation, most of its public interface should still *look/feel* like basic CRUD.

An important note about the API kernel -- with the "resolver" and "invoker" as pluggable parts, it's entirely possible that we could have a mix of APIs implemented by whatever technique we find most convenient, eg (a) keeping the current BAO/DAO implementations in some cases, (b) using entirely generic Doctrine wrappers in some cases, (c) using special purpose APIs written on top of Doctrine in other cases.

jaapjansma

  • I post frequently
  • ***
  • Posts: 247
  • Karma: 9
    • CiviCoop
  • CiviCRM version: 4.4.2
  • CMS version: Drupal 7
  • MySQL version: 5
  • PHP version: 5.4
Re: Doctrine and an API Kernel
March 25, 2014, 01:06:44 am
Hey Xavier,

I would argue that doctrine replaces the BAO logic as far as I understand BAO. Let me explain what doctrine does. It is a wrapper for storing and retrieving objects (not relational data) and that is why I think doctrine handles the BAO logic. because the BAO logic should be in the entity class. For example we should have an Entity class Contribution. This class holds information about the amount, receive date, status etc. but also a link to the source object (which could be an event, membership) and a link to the contact. Al those links are object-relationships. Then we have a standard repository class which we can override for the contribution. So we get an ContributionRepository. This repository is responsible for retrieving objects. Standard doctrine comes with functions as FindById, FindAll, FindAllById, everything after the FindBy and FindAllBy are properties of the entity class. So without any extra code one can do ContributionRespostiro->FindAllByContact($contactObject).
With lazy loading doctrine will load all linked objects to the contribution object.

To conclude the business logic of an entity goes into the entity class (BAO), the logic for fetching objects from a store (database) goes into a repository class (DAO).
Developer at Edeveloper / CiviCoop

xavier

  • Forum Godess / God
  • I’m (like) Lobo ;)
  • *****
  • Posts: 4453
  • Karma: 161
    • Tech To The People
  • CiviCRM version: yes probably
  • CMS version: drupal
Re: Doctrine and an API Kernel
March 25, 2014, 01:38:25 am
@jaap,

My question wasn't so much about fetching tons of related elements (seems than Doctrine can do magic here, thanks for the examples) but more for the business logic, like be sure you don't have any other primary email when you create/update one with is_primary set and that your promote another one if your remove the primary email.

@tim,
Presumably, some of the special code written for the custom api is also going to be using for forms. Do you have an idea to avoid the duplication of features between the form side and the api side? We tried to solve it by moving the magic into the BAO when we could so far.

Should we try to use more systematically the api also for the forms, or is this going to be done automatically when using backbone/whatever js mvc?

X+
-Hackathon and data journalism about the European parliament 24-26 jan. Watch out the result

jaapjansma

  • I post frequently
  • ***
  • Posts: 247
  • Karma: 9
    • CiviCoop
  • CiviCRM version: 4.4.2
  • CMS version: Drupal 7
  • MySQL version: 5
  • PHP version: 5.4
Re: Doctrine and an API Kernel
March 25, 2014, 02:12:47 am
@xavier: what we should do is to use validation rules on an entity and event listeners. So we can use the validation rule for checking an email address already extists. And the event listener on the event pre update of the e-mail adress entity to make sure there is one primary e-mailadress.

Developer at Edeveloper / CiviCoop

nicolas

  • I post occasionally
  • **
  • Posts: 92
  • Karma: 6
    • cividesk
  • CiviCRM version: 4.4 LTS
  • CMS version: Standalone (yep)
  • MySQL version: 5.1
  • PHP version: 5.3
Re: Doctrine and an API Kernel
April 01, 2014, 06:35:48 pm
There is in fact a 'business logic' layer that is now implemented in the BAO. It seems important to investigate how it is going to be implemented with Doctrine with concrete examples, and most probably do test implementations to look at performance, ease of use and security issues.

Example of 'business logic':
  • when a relationship is created between 2 contacts (ie. employment relationship), check if either contacts has an existing membership, and if this if memberships type allows inheritance on the same relationship type as the one created, and if there are less than the max number of inherited memberships already assigned, then create a related membership for the other contact
  • when a membership is renewed, if this corresponding membership type is rolling, and if the renewal happens before the end date or within the grace period, then extend the original membership for the duration specified in the membership type. Otherwise this is considered a new membership so leave the original membership as expired and create a new membership starting today and valid for the duration specified in the membership type
  • when creating an event registration, all the rules related to maximum capacity for an event, or any given price option chosen in the price set within the event registration, or requiring prior approval for the event ... all these rules decide on what actions are taken with this registration and the status it is in afterwards (ie. if we exceed max capacity the registration status will be 'Waitlisted', otherwise 'Confirmed')

These 'business rules' have to be 'played' and apply however the basic entities (relationship, membership or registration in the examples above) have been created, ie. through the GIU, an API call, an import, etc.

It seems like there are different potential approaches to implement 'business logic' in Doctrine:
- adding to the Doctrine objects with 'annotations'
- deriving/overriding Doctrine objects
- using event listeners
- and probably a few more.

What are the pros/cons of each? Are they complementary to one another and how? How would we choose one of the other to implement a particular business rule? Is there a 'standard' way to implement business layers with Doctrine or Symfony?

I have done a quick research and found that this seems to be a sore point for a number of people using Symfony and there seems to be no consensus, at least as far as I've seen (and as little I understood not being very familiar with either Symfony or Doctrine beyond concepts).
cividesk -- CiviCRM delivered ... your way!

jaapjansma

  • I post frequently
  • ***
  • Posts: 247
  • Karma: 9
    • CiviCoop
  • CiviCRM version: 4.4.2
  • CMS version: Drupal 7
  • MySQL version: 5
  • PHP version: 5.4
Re: Doctrine and an API Kernel
April 02, 2014, 02:12:08 am
Yes that is a very good approach. In my opinion there is a difference between a Doctrine Entity and an BAO for example. The BAO checks the business logic is chekced in BAO as soon as the object is saved also there are busniess logic methods to achieve something.. E.g. in the case the BAO Mergecase function is used to reassign a case to another client. In Doctrine I would assume that the function setClient, addClient, removeClient etc are used on the entity. And the logic for chaning something is hidden between those setters and getters. Also in Doctrine we have to think of valid entities in the system (not in the database). So one of the powerfull rules is that one can add validation constraints on an entity. And with the event listener one can insert their own constraints.

So we should start with writing down the entities (and the data they contain) and make their validation as simple as possible, as soon as it becomes a bit too complicated we should start thinking of putting those validation in their own 'Bundle' and inject them with event listeners and dependency injections.

A good rule of thumb here is that one should write down the responsibility of a class in a sentence of max 25 words. If the sentence becomes longer or to many 'and' or 'or' are used then one should split up the responsibility into different classes. This will result in many class files... (harder to find where a functionality is defined) but the functionality self is clearer to understand and much more maintainable. Also this creates loose coupled code.

As far as I am concerned I think at this moment the BAO has to much responsibility in the system. And it is not always clear which BAO is repsonible for something (e.g. I was making my own search the other day and I had to use the Contact BAO for the validation of the search form, while my search was not searching for contacts but for documents).
Developer at Edeveloper / CiviCoop

nicolas

  • I post occasionally
  • **
  • Posts: 92
  • Karma: 6
    • cividesk
  • CiviCRM version: 4.4 LTS
  • CMS version: Standalone (yep)
  • MySQL version: 5.1
  • PHP version: 5.3
Re: Doctrine and an API Kernel
April 02, 2014, 04:12:24 pm
Thinking about this, I like the idea of having the 'business logic' implemented as event listeners or hooks. This way, we could imagine CiviCRM extensions that would add to and/or replace the business logic provided by core. All it would require is access to the event listeners queue by the extensions.

This might have a number of advantages, including:
  • being able to modularize the core by moving certain features to 'system extensions', like maybe waiting lists for event registrations, maximum number of inherited memberships, etc. Kindda what was done with CiviDiscount. This makes for a modular, loosely coupled, more resilient core.
  • easy implementation of complex customer requirements: tax regulations that are so different from one country to another, auto assignement of contacts to regions and/or chapters based on where they live, complex memberships rules different than the default core rules, complex event registration processes like having family registrations (max of 5 attendees living at the same address), etc

I imagine Doctrine has a robust caching mechanism as all these event listeners might need to independently pull out very similar data to implement their respective business rules.
cividesk -- CiviCRM delivered ... your way!

totten

  • Administrator
  • Ask me questions
  • *****
  • Posts: 695
  • Karma: 64
Re: Doctrine and an API Kernel
April 02, 2014, 04:33:13 pm
+1 Agree that listeners/hooks would be really appealing and would loosen coupling.

There is a lot of caching involved in Doctrine. A few important caches:
 * Metadata cache. By storing the list of annotations (etc) in cache files, they avoid the need to use reflection and to parse annotations at runtime.
 * Within a given entity manager, there should only be one instance of the same entity. (IIRC -- Hibernate definitely does it this way, I'm 66% sure Doctrine does too). For example, in the following snippet, $a, $b, $c, and $d would refer to the same PHP object (rather than different objects with the same values).

Code: [Select]
$em = CRM_DB_EntityManager::singleton();
$a = $em->find('Contact', 1); // first read; hits DB
$b = $em->find('Contact', 1); // load from in-memory cache
$c = $em->find('Contact', 3)->getEmployer(); // load from in-memory cache
$d = $em->createQuery('SELECT c FROM Contact c WHERE c.id = 1')->getFirstResult(); // query DB then check in-memory cache

Pages: [1] 2
  • CiviCRM Community Forums (archive) »
  • Old sections (read-only, deprecated) »
  • Developer Discussion »
  • 5.0 Saloon »
  • Doctrine and an API Kernel

This forum was archived on 2017-11-26.