commit 95e518c03cfe58f3996b672b13a20bed00a5cceb
parent 7ea6026b9750726a32b727352b41bf8baaa6ce58
Author: Felicia Hummel <felicitus@felicitus.org>
Date: Wed, 17 Aug 2016 00:10:51 +0200
Merge pull request #715 from partkeepr/PartKeepr-685
Added an option to let associations be passed by reference and not by…
Diffstat:
7 files changed, 215 insertions(+), 27 deletions(-)
diff --git a/src/PartKeepr/DoctrineReflectionBundle/Annotation/ByReference.php b/src/PartKeepr/DoctrineReflectionBundle/Annotation/ByReference.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace PartKeepr\DoctrineReflectionBundle\Annotation;
+
+use Doctrine\ORM\Mapping\Annotation;
+
+/**
+ * Tells the system to pass the association by reference and not by value
+ *
+ * @Annotation
+ * @Target("PROPERTY")
+ */
+final class ByReference implements Annotation
+{
+}
diff --git a/src/PartKeepr/DoctrineReflectionBundle/Resources/views/model.js.twig b/src/PartKeepr/DoctrineReflectionBundle/Resources/views/model.js.twig
@@ -13,6 +13,7 @@ Ext.define('{{ className }}', {
{% for association in associations.MANY_TO_ONE %}
{ name: '{{ association.name }}',
reference: '{{ association.target }}'
+ {% if association.byReference %},byReference: true{% endif %}
}{% if not loop.last %},{% endif %}
{% endfor %}
diff --git a/src/PartKeepr/DoctrineReflectionBundle/Services/ReflectionService.php b/src/PartKeepr/DoctrineReflectionBundle/Services/ReflectionService.php
@@ -74,10 +74,10 @@ class ReflectionService
$associationMappings = $this->getDatabaseAssociationMappings($cm, $bTree);
$renderParams = [
- 'fields' => $fieldMappings,
+ 'fields' => $fieldMappings,
'associations' => $associationMappings,
- 'className' => $this->convertPHPToExtJSClassName($entity),
- 'parentClass' => $parentClass,
+ 'className' => $this->convertPHPToExtJSClassName($entity),
+ 'parentClass' => $parentClass,
];
$targetService = $this->reader->getClassAnnotation(
@@ -114,6 +114,7 @@ class ReflectionService
protected function getDatabaseAssociationMappings(ClassMetadata $cm, $bTree = false)
{
$associations = $cm->getAssociationMappings();
+ $byReferenceMappings = $this->getByReferenceMappings($cm);
$associationMappings = [];
@@ -154,10 +155,16 @@ class ReflectionService
// The self-referencing association may not be written for trees, because ExtJS can't load all nodes
// in one go.
if (!($bTree && $association['targetEntity'] == $cm->getName())) {
+ $byReference = false;
+
+ if (in_array($association['fieldName'], $byReferenceMappings)) {
+ $byReference = true;
+ }
$associationMappings[$associationType][] = [
- 'name' => $association['fieldName'],
- 'target' => $this->convertPHPToExtJSClassName($association['targetEntity']),
- 'getter' => $getter,
+ 'name' => $association['fieldName'],
+ 'target' => $this->convertPHPToExtJSClassName($association['targetEntity']),
+ 'byReference' => $byReference,
+ 'getter' => $getter,
'getterField' => $getterField,
];
}
@@ -195,6 +202,31 @@ class ReflectionService
}
/**
+ * Returns all by-reference associations.
+ *
+ * @param ClassMetadata $cm
+ *
+ * @return array
+ */
+ protected function getByReferenceMappings(ClassMetadata $cm)
+ {
+ $byReferenceMappings = [];
+
+ foreach ($cm->getReflectionClass()->getProperties() as $property) {
+ $byReferenceAnnotation = $this->reader->getPropertyAnnotation(
+ $property,
+ 'PartKeepr\DoctrineReflectionBundle\Annotation\ByReference'
+ );
+
+ if ($byReferenceAnnotation !== null) {
+ $byReferenceMappings[] = $property->getName();
+ }
+ }
+
+ return $byReferenceMappings;
+ }
+
+ /**
* Returns database field mappings.
*
* @param ClassMetadata $cm
diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraField.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraField.js
@@ -0,0 +1,20 @@
+/**
+ * Adds the config field "byReference" to a field.
+ *
+ * byReference tells the system not to serialize the whole item but only its reference.
+ */
+Ext.define('PartKeepr.data.HydraField', {
+ override: "Ext.data.field.Field",
+
+ byReference: false,
+
+ constructor: function (config)
+ {
+ if (config.byReference) {
+ this.byReference = config.byReference;
+ } else {
+ this.byReference = false;
+ }
+ this.callParent(arguments);
+ }
+});
diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraModel.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraModel.js
@@ -3,7 +3,7 @@ Ext.define("PartKeepr.data.HydraModel", {
mixins: ['PartKeepr.data.CallActions'],
- getData: function (options)
+ getData: function ()
{
var data = this.callParent(arguments);
@@ -13,7 +13,8 @@ Ext.define("PartKeepr.data.HydraModel", {
return data;
},
- get: function (fieldName) {
+ get: function (fieldName)
+ {
var ret, role, item;
ret = this.callParent(arguments);
@@ -22,7 +23,7 @@ Ext.define("PartKeepr.data.HydraModel", {
// The field is undefined, attempt to retrieve data via associations
var parts = fieldName.split(".");
- if (parts.length < 2) {
+ if (parts.length < 2) {
return ret;
}
@@ -39,6 +40,126 @@ Ext.define("PartKeepr.data.HydraModel", {
return ret;
},
/**
+ * Gets all of the data from this Models *loaded* associations. It does this
+ * recursively. For example if we have a User which hasMany Orders, and each Order
+ * hasMany OrderItems, it will return an object like this:
+ *
+ * {
+ * orders: [
+ * {
+ * id: 123,
+ * status: 'shipped',
+ * orderItems: [
+ * ...
+ * ]
+ * }
+ * ]
+ * }
+ *
+ * @param {Object} [result] The object on to which the associations will be added. If
+ * no object is passed one is created. This object is then returned.
+ * @param {Boolean/Object} [options] An object containing options describing the data
+ * desired.
+ * @param {Boolean} [options.associated=true] Pass `true` to include associated data from
+ * other associated records.
+ * @param {Boolean} [options.changes=false] Pass `true` to only include fields that
+ * have been modified. Note that field modifications are only tracked for fields that
+ * are not declared with `persist` set to `false`. In other words, only persistent
+ * fields have changes tracked so passing `true` for this means `options.persist` is
+ * redundant.
+ * @param {Boolean} [options.critical] Pass `true` to include fields set as `critical`.
+ * This is only meaningful when `options.changes` is `true` since critical fields may
+ * not have been modified.
+ * @param {Boolean} [options.persist] Pass `true` to only return persistent fields.
+ * This is implied when `options.changes` is set to `true`.
+ * @param {Boolean} [options.serialize=false] Pass `true` to invoke the `serialize`
+ * method on the returned fields.
+ * @return {Object} The nested data set for the Model's loaded associations.
+ */
+ getAssociatedData: function (result, options)
+ {
+ var me = this,
+ associations = me.associations,
+ deep, i, item, items, itemData, length,
+ record, role, roleName, opts, clear, associated;
+
+ result = result || {};
+
+ me.$gathering = 1;
+
+ if (options) {
+ options = Ext.Object.chain(options);
+ }
+
+ for (roleName in associations) {
+ role = associations[roleName];
+
+ item = role.getAssociatedItem(me);
+ if (!item || item.$gathering) {
+ continue;
+ }
+
+ if (item.isStore) {
+ item.$gathering = 1;
+
+ items = item.getData().items; // get the records for the store
+ length = items.length;
+ itemData = [];
+
+ for (i = 0; i < length; ++i) {
+ // NOTE - we don't check whether the record is gathering here because
+ // we cannot remove it from the store (it would invalidate the index
+ // values and misrepresent the content). Instead we tell getData to
+ // only get the fields vs descend further.
+ record = items[i];
+ deep = !record.$gathering;
+ record.$gathering = 1;
+ if (options) {
+ associated = options.associated;
+ if (associated === undefined) {
+ options.associated = deep;
+ clear = true;
+ } else {
+ if (!deep) {
+ options.associated = false;
+ clear = true;
+ }
+ }
+ opts = options;
+ } else {
+ opts = deep ? me._getAssociatedOptions : me._getNotAssociatedOptions;
+ }
+ itemData.push(record.getData(opts));
+ if (clear) {
+ options.associated = associated;
+ clear = false;
+ }
+ delete record.$gathering;
+ }
+
+ delete item.$gathering;
+ } else {
+ opts = options || me._getAssociatedOptions;
+ if (options && options.associated === undefined) {
+ opts.associated = true;
+ }
+
+ if (this.getField(roleName) !== null && this.getField(roleName).byReference) {
+ itemData = item.getId();
+ } else {
+ itemData = item.getData(opts);
+ }
+ }
+
+ result[roleName] = itemData;
+ }
+
+ delete me.$gathering;
+
+ return result;
+ },
+
+ /**
* Returns data from all associations
*
* @return {Object} An object containing the associations as properties
@@ -82,7 +203,7 @@ Ext.define("PartKeepr.data.HydraModel", {
getterName = this.associations[roleName].getterName;
store = this[getterName]();
- for (var i=0;i<data[roleName].length;i++) {
+ for (var i = 0; i < data[roleName].length; i++) {
// Delete existing IDs for duplicated data
if (data[roleName][i].isEntity) {
idProperty = data[roleName][i].idProperty;
diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Util/JsonWithAssociationsWriter.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Util/JsonWithAssociationsWriter.js
@@ -1,21 +1,19 @@
Ext.define("PartKeepr.JsonWithAssociations", {
- extend: 'Ext.data.writer.Json',
- alias: 'writer.jsonwithassociations',
+ extend: 'Ext.data.writer.Json',
+ alias: 'writer.jsonwithassociations',
- /**
- * @cfg {Array} associations Which associations to include.
- */
- associations: [],
- writeRecordId: false,
+ /**
+ * @cfg {Array} associations Which associations to include.
+ */
+ associations: [],
+ writeRecordId: false,
- getRecordData: function(record) {
- var me = this, i, key, subStore,
- data = me.callParent(arguments);
+ getRecordData: function (record)
+ {
+ var data = this.callParent(arguments);
- var storeName;
-
- Ext.apply(data, record.getAssociatedData());
+ Ext.apply(data, record.getAssociatedData());
- return data;
- }
-});-
\ No newline at end of file
+ return data;
+ }
+});
diff --git a/src/PartKeepr/ProjectBundle/Entity/ProjectPart.php b/src/PartKeepr/ProjectBundle/Entity/ProjectPart.php
@@ -4,6 +4,7 @@ namespace PartKeepr\ProjectBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use PartKeepr\CoreBundle\Entity\BaseEntity;
+use PartKeepr\DoctrineReflectionBundle\Annotation\ByReference;
use PartKeepr\DoctrineReflectionBundle\Annotation\TargetService;
use PartKeepr\PartBundle\Entity\Part;
use Symfony\Component\Serializer\Annotation\Groups;
@@ -21,6 +22,7 @@ class ProjectPart extends BaseEntity
*
* @ORM\ManyToOne(targetEntity="PartKeepr\PartBundle\Entity\Part", inversedBy="projectParts")
* @Groups({"default"})
+ * @ByReference
*
* @var Part
*/