Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Major refactoring. BC Break with existing sessions.

This brings better code, and a bug fix for a race condition.

It also brings better docs which explains a few potential issues.
  • Loading branch information...
commit 63721fefc7be994e97ab3b8146e7c220af8182eb 1 parent 9e4d00c
Magnus Nordlander authored
187  Collection/FlatteningParameterBag.php
... ...
@@ -0,0 +1,187 @@
  1
+<?php
  2
+
  3
+namespace Ebutik\MongoSessionBundle\Collection;
  4
+
  5
+use Ebutik\MongoSessionBundle\Escaper\Escaper;
  6
+
  7
+/**
  8
+* 
  9
+*/
  10
+abstract class FlatteningParameterBag
  11
+{
  12
+  protected $escaper;
  13
+
  14
+  protected function getEscaper()
  15
+  {
  16
+    if (!$this->escaper)
  17
+    {
  18
+      $this->escaper = $this->createEscaper();
  19
+    }
  20
+
  21
+    return $this->escaper;
  22
+  }
  23
+
  24
+  public function all()
  25
+  {
  26
+    $flattened = $this->_read();
  27
+    return $this->unflatten($flattened);
  28
+  }
  29
+
  30
+  public function keys()
  31
+  {
  32
+    return array_keys($this->all());
  33
+  }
  34
+
  35
+  public function replace(array $parameters = array())
  36
+  {
  37
+    $this->_write(array());
  38
+
  39
+    $this->add($parameters);
  40
+  }
  41
+
  42
+  public function add(array $parameters = array())
  43
+  {
  44
+    $current_data = $this->_read();
  45
+
  46
+    foreach ($parameters as $key => $value) 
  47
+    {
  48
+      $flat_kv_array = $this->flatten(array($key => $value));
  49
+      $current_data = array_merge($this->purgePrefix($current_data, $key), $flat_kv_array);
  50
+    }
  51
+
  52
+    $this->_write($current_data);
  53
+  }
  54
+
  55
+  public function get($key, $default = null)
  56
+  {
  57
+    $flattened = $this->readWithPrefix($key);
  58
+    $unflattened = $this->unflatten($flattened);
  59
+
  60
+    return (isset($unflattened[$key]) ? $unflattened[$key] : $default);
  61
+  }
  62
+
  63
+  public function set($key, $value)
  64
+  {
  65
+    $flat_kv_array = $this->flatten(array($key => $value));
  66
+
  67
+    $current_data = $this->_read();
  68
+
  69
+    $current_data = $this->purgePrefix($current_data, $key);
  70
+
  71
+    $this->_write(array_merge($current_data, $flat_kv_array));
  72
+  }
  73
+
  74
+  public function has($key)
  75
+  {
  76
+    $flattened = $this->readWithPrefix($key);
  77
+
  78
+    return count($flattened) > 0;
  79
+  }
  80
+
  81
+  public function remove($key)
  82
+  {
  83
+    $this->_write($this->purgePrefix($this->_read(), $key));
  84
+  }
  85
+
  86
+  protected function createEscaper()
  87
+  {
  88
+    $escaper = new Escaper();
  89
+    $escaper->setDelimiter('', '>');
  90
+    $escaper->setEscapingMap(array('/' => '%'));
  91
+
  92
+    return $escaper;
  93
+  }
  94
+
  95
+  protected function keyMatchesPrefix($key, $prefix)
  96
+  {
  97
+    return $key === $this->getEscaper()->escape($prefix) || strpos($key, $this->getEscaper()->escape($prefix).'/') === 0;
  98
+  }
  99
+
  100
+  protected function readWithPrefix($prefix)
  101
+  {
  102
+    $matches = array();
  103
+    foreach ($this->_read() as $key => $value)
  104
+    {
  105
+      if ($this->keyMatchesPrefix($key, $prefix))
  106
+      {
  107
+        $matches[$key] = $value;
  108
+      }
  109
+    }
  110
+    return $matches;
  111
+  }
  112
+
  113
+  protected function unflatten(array $data)
  114
+  {
  115
+    $result = array();
  116
+    foreach ($data as $key => $value) 
  117
+    {
  118
+      $exploded_key = explode('/', $key);
  119
+      $current_node = &$result;
  120
+      $last_index = count($exploded_key)-1;
  121
+
  122
+      foreach ($exploded_key as $index => $component) 
  123
+      {
  124
+        $component = $this->getEscaper()->unescape($component);
  125
+        if ($index < $last_index)
  126
+        {
  127
+          if (!isset($current_node[$component]))
  128
+          {
  129
+            $current_node[$component] = array();
  130
+          }
  131
+  
  132
+          if (!is_array($current_node[$component]))
  133
+          {
  134
+            throw new \RuntimeException(sprinf("Error while unflattening session data. Node %s of key %s already exists, but is not an array.", $component, $key));
  135
+          }
  136
+  
  137
+          $current_node = &$current_node[$component];          
  138
+        }
  139
+        else
  140
+        {
  141
+          $current_node[$component] = $value;
  142
+        }
  143
+      }
  144
+    }
  145
+
  146
+    return $result;
  147
+  }
  148
+
  149
+  protected function flatten(array $data)
  150
+  {
  151
+    $output = array();
  152
+    foreach ($data as $key => $subdata) 
  153
+    {
  154
+      if (is_array($subdata))
  155
+      {
  156
+        $processed_subdata = $this->flatten($subdata);
  157
+        foreach($processed_subdata as $subkey => $subsubdata)
  158
+        {
  159
+          $output[$this->getEscaper()->escape($key).'/'.$subkey] = $subsubdata;
  160
+        }
  161
+      }
  162
+      else
  163
+      {
  164
+        $output[$this->getEscaper()->escape($key)] = $subdata;
  165
+      }
  166
+    }
  167
+
  168
+    return $output;
  169
+  }
  170
+
  171
+  protected function purgePrefix($data, $prefix)
  172
+  {
  173
+    foreach ($data as $key => $value) 
  174
+    {
  175
+      if ($this->keyMatchesPrefix($key, $prefix))
  176
+      {
  177
+        unset($data[$key]);
  178
+      }
  179
+    }
  180
+
  181
+    return $data;
  182
+  }
  183
+
  184
+  abstract protected function _write(array $data);
  185
+
  186
+  abstract protected function _read();
  187
+}
4  DependencyInjection/Compiler/SetPrototypeClassPass.php
@@ -20,14 +20,14 @@ public function process(ContainerBuilder $container)
20 20
     $storage_def = $container->getDefinition('ebutik.mongosession.storage');
21 21
     $args = $storage_def->getArguments();
22 22
 
23  
-    $prototype_id = $args[4];
  23
+    $prototype_id = $args[3];
24 24
 
25 25
     if (!$container->hasDefinition($prototype_id)) {
26 26
       throw new \RuntimeException("MongoDB Session Prototype doesn't exist");
27 27
     }
28 28
 
29 29
     $prototype_def = $container->getDefinition($prototype_id);
30  
-    $args[3] = $prototype_def->getClass();
  30
+    $args[2] = $prototype_def->getClass();
31 31
 
32 32
     $storage_def->setArguments($args);
33 33
   }
202  Document/Session.php
@@ -2,15 +2,12 @@
2 2
 
3 3
 namespace Ebutik\MongoSessionBundle\Document;
4 4
 
5  
-use Doctrine\Common\Collections\ArrayCollection;
6 5
 use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
7 6
 
8  
-use Ebutik\MongoSessionBundle\Interfaces\SessionEmbeddable;
9  
-
10 7
 /**
11  
- * 
12 8
  * @MongoDB\Document(repositoryClass="Ebutik\MongoSessionBundle\Repository\SessionRepository")
13  
- * 
  9
+ * @MongoDB\ChangeTrackingPolicy("DEFERRED_EXPLICIT") 
  10
+ *
14 11
  * @author Magnus Nordlander
15 12
  */
16 13
 class Session
@@ -32,19 +29,9 @@ class Session
32 29
   protected $accessed_at;
33 30
 
34 31
   /**
35  
-   * @MongoDB\Hash
36  
-   */
37  
-  protected $scalar_attributes = array();
38  
-
39  
-  /**
40  
-   * @MongoDB\EmbedMany(targetDocument="Ebutik\MongoSessionBundle\Document\EmbeddableSessionAttributeWrapper")
41  
-   */
42  
-  protected $embeddable_attributes;
43  
-
44  
-  /**
45  
-   * @MongoDB\Hash
46  
-   */
47  
-  protected $serialized_attributes = array();
  32
+   * @MongoDB\EmbedOne(targetDocument="Ebutik\MongoSessionBundle\Document\SessionAttributeBag")
  33
+   */  
  34
+  protected $attribute_bag;
48 35
 
49 36
   /**
50 37
    * @author Magnus Nordlander
@@ -52,11 +39,16 @@ class Session
52 39
   public function __construct()
53 40
   {
54 41
     $this->generateId();
55  
-    $this->embeddable_attributes = new ArrayCollection;
  42
+    $this->attribute_bag = new SessionAttributeBag();
56 43
     $this->created_at = new \DateTime();
57 44
     $this->updateAccessTime();
58 45
   }
59 46
 
  47
+  public function getAttributeBag()
  48
+  {
  49
+    return $this->attribute_bag;
  50
+  }
  51
+
60 52
   /**
61 53
    * @author Magnus Nordlander
62 54
    * @MongoDB\PostLoad
@@ -77,21 +69,6 @@ private function generateId()
77 69
   /**
78 70
    * @author Magnus Nordlander
79 71
    **/
80  
-  protected function findEmbeddableAttributeWrapper($key)
81  
-  {
82  
-    foreach ($this->embeddable_attributes as $wrapper)
83  
-    {
84  
-      if ($wrapper->getKey() == $key)
85  
-      {
86  
-        return $wrapper;
87  
-      }
88  
-    }
89  
-    return null;
90  
-  }
91  
-
92  
-  /**
93  
-   * @author Magnus Nordlander
94  
-   **/
95 72
   public function getId()
96 73
   {
97 74
     return $this->id;
@@ -99,131 +76,6 @@ public function getId()
99 76
 
100 77
   /**
101 78
    * @author Magnus Nordlander
102  
-   **/
103  
-  public function read($key)
104  
-  {
105  
-    if (isset($this->scalar_attributes[$key]))
106  
-    {
107  
-      return $this->scalar_attributes[$key];
108  
-    }
109  
-    else if (isset($this->serialized_attributes[$key]))
110  
-    {
111  
-      return unserialize($this->scalar_attributes[$key]);
112  
-    }
113  
-    else if ($wrapper = $this->findEmbeddableAttributeWrapper($key))
114  
-    {
115  
-      return $wrapper->getAttribute();
116  
-    }
117  
-    else
118  
-    {
119  
-      return null;
120  
-    }
121  
-  }
122  
-
123  
-  /**
124  
-   * @author Magnus Nordlander
125  
-   **/
126  
-  public function readAll()
127  
-  {
128  
-    return array_merge(
129  
-      $this->scalar_attributes,
130  
-      array_map('unserialize', $this->serialized_attributes)
131  
-//      $this->getEmbeddableAttributeArray()
132  
-    );
133  
-  }
134  
-
135  
-  /**
136  
-   * @author Magnus Nordlander
137  
-   **/
138  
-  public function getEmbeddableAttributeArray()
139  
-  {
140  
-    $out = array();
141  
-    foreach ($this->embeddable_attributes as $wrapper)
142  
-    {
143  
-      $out[$wrapper->getKey()] = $wrapper->getAttribute();
144  
-    }
145  
-
146  
-    return $out;
147  
-  }
148  
-
149  
-  /**
150  
-   * @author Magnus Nordlander
151  
-   **/
152  
-  public function remove($key)
153  
-  {
154  
-    $retval = null;
155  
-
156  
-    if (isset($this->scalar_attributes[$key]))
157  
-    {
158  
-      $retval = $this->scalar_attributes[$key];
159  
-      unset($this->scalar_attributes[$key]);
160  
-    }
161  
-    else if (isset($this->serialized_attributes[$key]))
162  
-    {
163  
-      $retval = unserialize($this->serialized_attributes[$key]);
164  
-      unset($this->serialized_attributes[$key]);
165  
-    }
166  
-    else if ($wrapper = $this->findEmbeddableAttributeWrapper($key))
167  
-    {
168  
-      $retval = $wrapper->getAttribute();
169  
-      $this->embeddable_attributes->removeElement($wrapper);
170  
-    }
171  
-    else
172  
-    {
173  
-      return null;
174  
-    }
175  
-
176  
-    return $retval;
177  
-  }
178  
-  
179  
-  /**
180  
-   * @author Joakim Friberg
181  
-   */
182  
-  public function clear()
183  
-  {
184  
-    $this->scalar_attributes = array();
185  
-    $this->serialized_attributes = array();
186  
-    $this->embeddable_attributes->clear();
187  
-  }
188  
-
189  
-  /**
190  
-   * @author Magnus Nordlander
191  
-   **/
192  
-  public function write($key, $data)
193  
-  {
194  
-    $this->remove($key);
195  
-
196  
-    if ($data instanceOf SessionEmbeddable)
197  
-    {
198  
-      $this->embeddable_attributes->add(new EmbeddableSessionAttributeWrapper($key, $data));
199  
-    }
200  
-    else if (is_scalar($data) || $data === null)
201  
-    {
202  
-      $this->scalar_attributes[$key] = $data;
203  
-    }
204  
-    else if (is_object($data))
205  
-    {
206  
-      $this->serialized_attributes[$key] = serialize($data);
207  
-    }
208  
-    else if (is_array($data))
209  
-    {
210  
-      if (self::arrayOnlyContainsScalarsRecursive($data))
211  
-      {
212  
-        $this->scalar_attributes[$key] = $data;
213  
-      }
214  
-      else
215  
-      {
216  
-        $this->serialized_attributes[$key] = serialize($data);
217  
-      }
218  
-    }
219  
-    else
220  
-    {
221  
-      throw new \RuntimeError("Data of type ".gettype($data)." cannot be saved in the session");
222  
-    }
223  
-  }
224  
-
225  
-  /**
226  
-   * @author Magnus Nordlander
227 79
    * @see http://www.doctrine-project.org/docs/orm/2.0/en/cookbook/implementing-wakeup-or-clone.html
228 80
    **/
229 81
   public function __clone()
@@ -233,39 +85,9 @@ public function __clone()
233 85
     {
234 86
       $this->generateId();
235 87
 
236  
-      $new_array = new ArrayCollection;
237  
-      foreach( $this->embeddable_attributes as $key => $wrapper ) {
238  
-        $new_array->add(new EmbeddableSessionAttributeWrapper($wrapper->getKey(), clone $wrapper->getAttribute()));
239  
-      }
240  
-      $this->embeddable_attributes = $new_array;
241  
-
  88
+      $this->attribute_bag = clone $this->attribute_bag;
242 89
     }
243 90
     // otherwise do nothing, do NOT throw an exception!
244 91
   }
245 92
 
246  
-  /**
247  
-   * @author Magnus Nordlander
248  
-   **/
249  
-  static protected function arrayOnlyContainsScalarsRecursive(array $array)
250  
-  {
251  
-    $callback = function($reduced, $item) use (&$callback)
252  
-    {
253  
-      if ($reduced == false)
254  
-      {
255  
-        return false;
256  
-      }
257  
-      else if (is_array($item))
258  
-      {
259  
-        return array_reduce($item, $callback, true);
260  
-      }
261  
-      else if (is_scalar($item))
262  
-      {
263  
-        return true;
264  
-      }
265  
-
266  
-      return false;
267  
-    };
268  
-
269  
-    return array_reduce($array, $callback, true);
270  
-  }
271 93
 }
124  Document/SessionAttributeBag.php
... ...
@@ -0,0 +1,124 @@
  1
+<?php
  2
+
  3
+namespace Ebutik\MongoSessionBundle\Document;
  4
+
  5
+use Ebutik\MongoSessionBundle\Collection\FlatteningParameterBag;
  6
+
  7
+use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
  8
+
  9
+use Doctrine\Common\Collections\ArrayCollection;
  10
+
  11
+use Ebutik\MongoSessionBundle\Interfaces\SessionEmbeddable;
  12
+
  13
+/**
  14
+ * @MongoDB\EmbeddedDocument
  15
+ */
  16
+class SessionAttributeBag extends FlatteningParameterBag
  17
+{
  18
+  /**
  19
+   * @MongoDB\Id
  20
+   * 
  21
+   * This attribute isn't REALLY needed, however, it's nice, because it makes things 
  22
+   * easier in __clone, and it allows us to work around MODM-160.
  23
+   */
  24
+  protected $id;
  25
+
  26
+  /**
  27
+   * @MongoDB\Hash
  28
+   */
  29
+  protected $scalar_attributes = array();
  30
+
  31
+  /**
  32
+   * @MongoDB\Hash
  33
+   */
  34
+  protected $serialized_attributes = array();
  35
+
  36
+  /**
  37
+   * @MongoDB\EmbedMany(targetDocument="Ebutik\MongoSessionBundle\Document\EmbeddableSessionAttributeWrapper")
  38
+   */
  39
+  protected $embeddable_attributes;
  40
+
  41
+  public function __construct()
  42
+  {
  43
+    $this->embeddable_attributes = new ArrayCollection();
  44
+  }
  45
+
  46
+  /**
  47
+   * @author Magnus Nordlander
  48
+   * @see http://www.doctrine-project.org/docs/orm/2.0/en/cookbook/implementing-wakeup-or-clone.html
  49
+   **/
  50
+  public function __clone()
  51
+  {
  52
+    // If the entity has an identity, proceed as normal.
  53
+    if ($this->id) 
  54
+    {
  55
+      $new_embeddables = new ArrayCollection;
  56
+      foreach( $this->embeddable_attributes as $key => $wrapper ) {
  57
+        $new_embeddables->add(new EmbeddableSessionAttributeWrapper($wrapper->getKey(), clone $wrapper->getAttribute()));
  58
+      }
  59
+      $this->embeddable_attributes = $new_embeddables;
  60
+    }
  61
+    // otherwise do nothing, do NOT throw an exception!
  62
+  }
  63
+
  64
+  protected function clear()
  65
+  {
  66
+    $this->scalar_attributes = array();
  67
+    $this->serialized_attributes = array();
  68
+    $this->embeddable_attributes->clear();
  69
+  }
  70
+
  71
+  protected function _write(array $data)
  72
+  {
  73
+    $this->clear();
  74
+
  75
+    foreach ($data as $key => $subdata)
  76
+    {
  77
+      if ($subdata instanceOf SessionEmbeddable)
  78
+      {
  79
+        $this->embeddable_attributes->add(new EmbeddableSessionAttributeWrapper($key, $subdata));
  80
+      }
  81
+      else if (is_scalar($subdata) || $subdata === null)
  82
+      {
  83
+        $this->scalar_attributes[$key] = $subdata;
  84
+      }
  85
+      else if (is_object($subdata))
  86
+      {
  87
+        $this->serialized_attributes[$key] = serialize($subdata);
  88
+      }
  89
+      else
  90
+      {
  91
+        throw new \RuntimeError("Data of type ".gettype($subdata)." cannot be saved in the session");
  92
+      }
  93
+    }
  94
+  }
  95
+
  96
+  protected function getKeyValueArrayForEmbeddedObjects()
  97
+  {
  98
+    $array = array();
  99
+
  100
+    foreach ($this->embeddable_attributes as $wrapper) 
  101
+    {
  102
+      $array[$wrapper->getKey()] = $wrapper->getAttribute();
  103
+    }
  104
+
  105
+    return $array;
  106
+  }
  107
+
  108
+  protected function _read()
  109
+  {
  110
+    return array_merge(
  111
+      $this->scalar_attributes,
  112
+      array_map('unserialize', $this->serialized_attributes),
  113
+      $this->getKeyValueArrayForEmbeddedObjects()
  114
+    );
  115
+  }
  116
+
  117
+  protected function createEscaper()
  118
+  {
  119
+    $escaper = parent::createEscaper();
  120
+    $escaper->setEscapingMap(array('/' => '%', '.' => ':'));
  121
+
  122
+    return $escaper;
  123
+  }
  124
+}
1  Escaper/Escaper.php
@@ -13,7 +13,6 @@ class Escaper implements EscaperInterface
13 13
    * @var array
14 14
    */
15 15
   protected $map = array('.' => '-'); // TODO remove the default value when the patch (https://github.com/symfony/symfony/pull/2427) will be applied
16  
-
17 16
   /**
18 17
    * @var boolean
19 18
    */
10  EventListener/MongoSessionListener.php
@@ -10,6 +10,8 @@
10 10
 use Symfony\Component\HttpKernel\Event\GetResponseEvent;
11 11
 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
12 12
 
  13
+use Symfony\Component\HttpFoundation\Session as SymfonySession;
  14
+
13 15
 use Symfony\Component\HttpFoundation\Cookie;
14 16
 
15 17
 /**
@@ -20,13 +22,15 @@
20 22
 class MongoSessionListener
21 23
 {
22 24
   protected $storage;
  25
+  protected $symfony_session;
23 26
   
24 27
   /**
25 28
    * @author Magnus Nordlander
26 29
    **/
27  
-  public function __construct(MongoODMSessionStorage $storage)
  30
+  public function __construct(MongoODMSessionStorage $storage, SymfonySession $session)
28 31
   {
29 32
     $this->storage = $storage;
  33
+    $this->symfony_session = $session;
30 34
   }
31 35
   
32 36
   /**
@@ -70,6 +74,10 @@ public function onKernelResponse(FilterResponseEvent $event)
70 74
                                                    $options['domain'],
71 75
                                                    $options['secure'],
72 76
                                                    $options['httponly']));
  77
+
  78
+          $this->symfony_session->save();
  79
+          // Prevent race conditions by closing early.
  80
+          $this->symfony_session->close();
73 81
         }
74 82
         catch (\RuntimeException $e)
75 83
         {
10  Interfaces/SessionEmbeddable.php
@@ -4,5 +4,13 @@
4 4
 
5 5
 interface SessionEmbeddable
6 6
 {
7  
-  
  7
+  /* 
  8
+   This interface doesn't contain any required methods.
  9
+   The requirement you should adhere to is that the class
  10
+   is an embedded document.
  11
+
  12
+   PLEASE NOTE! As long as MODM-160 remains unresolved you
  13
+   also need to have an id on the embeddable object, as well as
  14
+   embedded children.
  15
+  */
8 16
 }
9  Resources/config/mongosession.xml
@@ -12,7 +12,6 @@
12 12
   <services>
13 13
       <service id="ebutik.mongosession.storage" class="%ebutik.mongosession.storage.class%">
14 14
         <argument type="service" id="ebutik.mongosession.document_manager"/>
15  
-        <argument type="service" id="ebutik.mongosession.key_escaper"/>
16 15
         <argument>%session.storage.options%</argument>
17 16
 
18 17
         <call method="setContainer">
@@ -24,16 +23,10 @@
24 23
 
25 24
       <service id="ebutik.mongosession.listener" class="%ebutik.mongosession.listener.class%">
26 25
         <argument type="service" id="ebutik.mongosession.storage" />
  26
+        <argument type="service" id="session" />
27 27
 
28 28
         <tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="50" />
29 29
         <tag name="kernel.event_listener" event="kernel.response" method="onKernelResponse" priority="-50" />
30 30
       </service>
31  
-
32  
-      <service id="ebutik.mongosession.key_escaper" class="%ebutik.mongosession.key_escaper.class%">
33  
-        <call method="setDelimiter">
34  
-          <argument></argument>
35  
-          <argument>&gt;</argument>
36  
-        </call>
37  
-      </service>
38 31
   </services>
39 32
 </container>
110  Resources/doc/index.rst
Source Rendered
@@ -10,15 +10,96 @@ Doctrine MongoDB ODM. Special features include:
10 10
 
11 11
 Configuration
12 12
 -------------
  13
+You'll need to change your session configuration's storage_id to 
  14
+ebutik.mongo_session.storage. Since the bundle includes ODM Document classes, 
  15
+remember to add the bundle to your mappings, unless you're using auto_mapping.
  16
+
13 17
 Below is the default configuration, you don't need to change it unless it doesn't
14 18
 suit your needs::
15 19
 
16 20
     ebutik_mongo_session:
17 21
         document_manager: default
  22
+        session_prototype_id: ebutik.mongosession.session.prototype
  23
+        strict_request_checking: false
  24
+        purge_probability_divisor: 30
18 25
 
19  
-You'll also need to change your session configuration's storage_id to 
20  
-ebutik.mongo_session.storage. Since the bundle includes ODM Document classes, 
21  
-remember to add the bundle to your mappings, unless you're using auto_mapping.
  26
+The document_manager parameter should be pretty obvious. It's just the name
  27
+of whichever document_manager you wish to use.
  28
+
  29
+The session_prototype_id parameter is the DIC ID of a prototype service used
  30
+to create a new session. That way, if you want to do any changes to the session
  31
+document, your changes can be confined easily. It also allows a good amount of
  32
+changes without subclassing, just by adding calls to the service.
  33
+
  34
+Usually you don't need to change the strict_request_checking, however, in order
  35
+to avoid some design decisions in Symfony, the Session storage directly accesses
  36
+the $_COOKIES array. Unless, that is, if this configuration parameter is true. If
  37
+it is, you'll need to set the request session id on the storage yourself. See
  38
+MongoODMSessionStorage::getRequestSessionId for more information.
  39
+
  40
+Because writes are relatively expensive, it's usually unnecessary to purge old 
  41
+sessions every request. The purge_probability_divisor controls how likely
  42
+it is that a given request will purge old sessions. If the value is 30, that 
  43
+means that on a given request, there's a 1 in 30 chance that the old sessions
  44
+will be purged. If the value is 1, that means the chance is 1 in 1, i.e. that
  45
+the old sessions will be purged on every request. You may wish to tune this
  46
+depending on your traffic. 
  47
+
  48
+A very important note on document managers
  49
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  50
+Choosing your document manager may seem like an easy choice. You just use the
  51
+default one, right? That way you don't have to worry? Wrong.
  52
+
  53
+Using the default document manager with this bundle is a dangerous choice 
  54
+which requires you to tread carefully. Currently (and until MODM-160 is 
  55
+resolved) this bundle may attach a document to your document manager when
  56
+the session is started (which may be quite early), and it flushes the 
  57
+document manager after the response has been returned.
  58
+
  59
+During this time, you may not clear the document manager, since this would
  60
+deattach the session.
  61
+
  62
+You might think that this is no big deal, that you'll just not use sessions
  63
+when doing batch processing. While the latter is prudent, it's still a big deal.
  64
+
  65
+As you may have read in the MongoDB ODM Docs, the default change tracking
  66
+policy is DEFERRED_IMPLICIT. What this means is that every time you flush
  67
+the document manager, it checks every attached document to see if it has any
  68
+changes. If it does, the changes are saved to MongoDB.
  69
+
  70
+This might seem convenient to you, but if you're using the Symfony Form
  71
+Component together with the Symfony Validation Component, it is none of the
  72
+sort. You see, when you bind the data to a document using the form component
  73
+you change the document. Usually you then validate the document using the 
  74
+validation component. If the document is valid, you flush the Document
  75
+Manager, to save the changes.
  76
+
  77
+This is very dangerous when you have a bundle which flushes the document
  78
+manager on *every* request involving a session (or even if you for some 
  79
+other reason flush the document manager in a response listener). 
  80
+
  81
+There are three potential solutions to this. One of them is great, but
  82
+cannot be implemented due to MODM-160. It involves deattaching the session
  83
+after fetching it, and then when it's time to save the session, to clear
  84
+the document manager, merging (i.e. re-attaching) the session back into the
  85
+document manager, and then flushing it. Sadly, you cannot merge documents
  86
+with more then one level of embedded documents into your document manager
  87
+without it acting up. This is a bug, which has been reported as MODM-160.
  88
+
  89
+The second solution is to use a separate document manager for the sessions.
  90
+If you do, you won't experience any problems, since sessions will be the
  91
+only thing ever attached to that document manager. However, this makes 
  92
+reference relations between embedded objects and other documents difficult.
  93
+
  94
+The third solution is to use the DEFERRED_EXPLICIT change tracking policy.
  95
+Using this policy, the Document Manager won't check your documents for
  96
+changes unless they've been explicitly persisted. Since you shouldn't
  97
+persist any invalid documents, this seems like a good deal. However, it
  98
+might be tedious to implement if you have a lot of already written code.
  99
+
  100
+Which solution you choose is up to you (although for many reasons I'd
  101
+recommend you to use DEFERRED_EXPLICIT anyway). Once we're able, we 
  102
+will implement the first solution in this bundle, putting an end to this.
22 103
 
23 104
 Usage
24 105
 -----
@@ -40,4 +121,25 @@ class is an embeddable document that is to be embedded in the session.
40 121
 
41 122
 Please note that the interface itself doesn't contain any requirements that 
42 123
 the implementing class actually is an embedded document, but if the class
43  
-isn't, you *will* get errors when saving it to the session.
  124
+isn't, you *will* get errors when saving it to the session. The embedded
  125
+object is also cloned, so if you have further embeds, you'll want to 
  126
+implement __clone.
  127
+
  128
+An example session embedded object could look like this::
  129
+
  130
+    <?php
  131
+
  132
+    use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoDB;
  133
+
  134
+    use Ebutik\MongoSessionBundle\Interfaces\SessionEmbeddable;
  135
+
  136
+    /**
  137
+     * @MongoDB\EmbeddedDocument
  138
+     */
  139
+    class SessionEmbeddableDocument implements SessionEmbeddable
  140
+    {
  141
+      /**
  142
+       * @MongoDB\Field(type="string")
  143
+       */
  144
+      protected $something_else;
  145
+    }
71  SessionStorage/MongoODMSessionStorage.php
@@ -7,8 +7,6 @@
7 7
 use Symfony\Component\DependencyInjection\ContainerAwareInterface;
8 8
 use Symfony\Component\DependencyInjection\ContainerInterface;
9 9
 
10  
-use Ebutik\MongoSessionBundle\Escaper\EscaperInterface;
11  
-
12 10
 use Ebutik\MongoSessionBundle\Interfaces\SessionEmbeddable;
13 11
 
14 12
 use Doctrine\ODM\MongoDB\DocumentManager;
@@ -24,11 +22,6 @@ class MongoODMSessionStorage implements SessionStorageInterface, ContainerAwareI
24 22
   protected $dm;
25 23
 
26 24
   /**
27  
-   * @var EscaperInterface
28  
-   */
29  
-  protected $key_escaper;
30  
-
31  
-  /**
32 25
    * @var array
33 26
    */
34 27
   protected $options;
@@ -73,12 +66,10 @@ class MongoODMSessionStorage implements SessionStorageInterface, ContainerAwareI
73 66
   /**
74 67
    * @author Magnus Nordlander
75 68
    **/
76  
-  public function __construct(DocumentManager $dm, EscaperInterface $key_escaper, array $options, $session_class, $session_prototype_id, $strict_request_checking = false, $purge_probability_divisor = 30)
  69
+  public function __construct(DocumentManager $dm, array $options, $session_class, $session_prototype_id, $strict_request_checking = false, $purge_probability_divisor = 30)
77 70
   {
78 71
     $this->dm = $dm;
79 72
 
80  
-    $this->key_escaper = $key_escaper;
81  
-
82 73
     $this->setOptions($options);
83 74
 
84 75
     $this->session_class = $session_class;
@@ -173,10 +164,6 @@ public function start()
173 164
       {
174 165
         $this->session = $this->container->get($this->session_prototype_id);
175 166
       }
176  
-      else
177  
-      {
178  
-        //$this->dm->detach($this->session);
179  
-      }
180 167
     }
181 168
   }
182 169
 
@@ -215,23 +202,15 @@ public function read($key)
215 202
       throw new \RuntimeException("The session is not started yet.");
216 203
     }
217 204
 
218  
-    if ($key != '_symfony2')
219  
-    {
220  
-      throw new \RuntimeException("This storage only stores Symfony2 data");
221  
-    }
  205
+    $data = $this->session->getAttributeBag()->get($key);
222 206
 
223  
-    $attributes = array('attributes' => array(), 'flashes' => array(), 'locale' => null);
224  
-    foreach($this->session->readAll() as $attribute_key => $attribute_value)
225  
-    {
226  
-      $attributes[$this->key_escaper->unescape($attribute_key)] = $attribute_value;
227  
-    }
228  
-    // Ugly temporary hack to handle changes in Symfony's session handling
229  
-    foreach ($this->session->getEmbeddableAttributeArray() as $key => $value) 
  207
+    // Fix SF2 semi-bug
  208
+    if ($key == '_symfony2' && !isset($data['flashes']))
230 209
     {
231  
-      $attributes['attributes'][$this->key_escaper->unescape($key)] = $value;
  210
+      $data['flashes'] = array();
232 211
     }
233 212
 
234  
-    return $attributes;
  213
+    return $data;
235 214
   }
236 215
 
237 216
   /**
@@ -252,15 +231,7 @@ public function remove($key)
252 231
       throw new \RuntimeException("The session is not started yet.");
253 232
     }
254 233
 
255  
-    if ($key != '_symfony2')
256  
-    {
257  
-      throw new \RuntimeException("This storage only stores Symfony2 data");
258  
-    }
259  
-
260  
-    foreach ($data as $subkey) 
261  
-    {
262  
-      $this->session->remove($subkey);
263  
-    }
  234
+    $this->session->getAttributeBag()->remove($key);
264 235
   }
265 236
 
266 237
   /**
@@ -280,30 +251,8 @@ public function write($key, $data)
280 251
       throw new \RuntimeException("The session is not started yet.");
281 252
     }
282 253
 
283  
-    if ($key != '_symfony2')
284  
-    {
285  
-      throw new \RuntimeException("This storage only stores Symfony2 data");
286  
-    }
287  
-    
288  
-    $this->session->clear();
  254
+    $this->session->getAttributeBag()->set($key, $data);
289 255
 
290  
-    foreach ($data as $subkey => $value) 
291  
-    {
292  
-      if ($subkey == 'attributes')
293  
-      {
294  
-        // Ugly temporary hack to handle changes in Symfony's session handling
295  
-        foreach ($value as $attribute => $attribute_value)
296  
-        {
297  
-          if ($attribute_value instanceof SessionEmbeddable)
298  
-          {
299  
-            unset($value[$attribute]);
300  
-            $this->session->write($this->key_escaper->escape($attribute), $attribute_value);
301  
-          }
302  
-        }
303  
-      }
304  
-      $this->session->write($this->key_escaper->escape($subkey), $value);
305  
-    }
306  
-    
307 256
     $this->flush();
308 257
   }
309 258
 
@@ -340,12 +289,8 @@ public function flush()
340 289
   {
341 290
     if ($this->session)
342 291
     {
343  
-//      $this->dm->clear();
344  
-//      $merged = $this->dm->merge($this->session);
345  
-//      $this->dm->persist($merged);
346 292
       $this->dm->persist($this->session);
347 293
       $this->dm->flush();
348  
-//      $this->dm->detach($merged);
349 294
     }
350 295
   }
351 296
 }
173  Tests/Collection/FlatteningParameterBagTest.php
... ...
@@ -0,0 +1,173 @@
  1
+<?php
  2
+
  3
+namespace Ebutik\MongoSessionBundle\Test\Collection;
  4
+
  5
+use Ebutik\MongoSessionBundle\Collection\FlatteningParameterBag;
  6
+
  7
+use Mockery as M;
  8
+
  9
+/**
  10
+* 
  11
+*/
  12
+class FlatteningParameterBagTest extends \PHPUnit_Framework_TestCase
  13
+{
  14
+  public function setUp()
  15
+  {
  16
+    $this->bag = new FlatteningParameterBagMockProxy(M::mock());
  17
+  }
  18
+
  19
+  public function testSetFlat()
  20
+  {
  21
+    $this->bag->mock->shouldReceive(array('_read' => array()));
  22
+    $this->bag->mock->shouldReceive('_write')->with(array('foo' => 'bar'))->once();
  23
+
  24
+    $this->bag->set('foo', 'bar');
  25
+  }
  26
+
  27
+  public function testSetDeep()
  28
+  {
  29
+    $this->bag->mock->shouldReceive(array('_read' => array()));
  30
+    $this->bag->mock->shouldReceive('_write')->with(array('foo/bar' => 'baz'))->once();
  31
+
  32
+    $this->bag->set('foo', array('bar' => 'baz'));
  33
+  }
  34
+
  35
+  public function testSetToReplace()
  36
+  {
  37
+    $this->bag->mock->shouldReceive(array('_read' => array('foo/bar' => 'baz')));
  38
+    $this->bag->mock->shouldReceive('_write')->with(array('foo/quux' => 'zoinks'))->once();
  39
+
  40
+    $this->bag->set('foo', array('quux' => 'zoinks'));
  41
+  }
  42
+
  43
+  public function testSetWithSlash()
  44
+  {
  45
+    $this->bag->mock->shouldReceive(array('_read' => array()));
  46
+    $this->bag->mock->shouldReceive('_write')->with(array('foo%>baz' => 'bar'))->once();
  47
+
  48
+    $this->bag->set('foo/baz', 'bar');
  49
+  }
  50
+
  51
+  public function testGetFlat()
  52
+  {
  53
+    $this->bag->mock->shouldReceive(array('_read' => array('foo' => 'bar')));
  54
+
  55
+    $this->assertEquals('bar', $this->bag->get('foo'));
  56
+  }
  57
+
  58
+  public function testGetDeep()
  59
+  {
  60
+    $this->bag->mock->shouldReceive(array('_read' => array('foo/bar' => 'baz')));
  61
+
  62
+    $this->assertEquals(array('bar' => 'baz'), $this->bag->get('foo'));
  63
+  }
  64
+
  65
+  public function testGetDefault()
  66
+  {
  67
+    $this->bag->mock->shouldReceive(array('_read' => array()));
  68
+
  69
+    $this->assertEquals('bar', $this->bag->get('foo', 'bar'));
  70
+  }
  71
+
  72
+  public function testGetWithSlash()
  73
+  {
  74
+    $this->bag->mock->shouldReceive(array('_read' => array('foo%>baz' => 'bar')));
  75
+
  76
+    $this->assertEquals('bar', $this->bag->get('foo/baz'));
  77
+  }
  78
+
  79
+  public function testGetAll()
  80
+  {
  81
+    $this->bag->mock->shouldReceive(array('_read' => array('foo/bar' => 'baz', 'foo/baz' => 'bar', 'quux' => 'zoinks')));
  82
+
  83
+    $this->assertEquals(array('foo' => array('baz' => 'bar', 'bar' => 'baz'), 'quux' => 'zoinks'), $this->bag->all());
  84
+  }
  85
+
  86
+  public function testGetKeys()
  87
+  {
  88
+    $this->bag->mock->shouldReceive(array('_read' => array('foo/bar' => 'baz', 'foo/baz' => 'bar', 'quux' => 'zoinks')));
  89
+
  90
+    $this->assertEquals(array('foo', 'quux'), $this->bag->keys());
  91
+  }
  92
+
  93
+  public function testAdd()
  94
+  {
  95
+    $this->bag->mock->shouldReceive(array('_read' => array('baz/bar' => 'baz')));
  96
+    $this->bag->mock->shouldReceive('_write')->with(array('baz/bar' => 'baz', 'quux' => 'zoinks', 'foo/bar' => 'baz'))->once();
  97
+
  98
+    $this->bag->add(array('quux' => 'zoinks', 'foo' => array('bar' => 'baz')));
  99
+  }
  100
+
  101
+  public function testReplace()
  102
+  {
  103
+    $this->bag->mock->shouldReceive('_write')->with(array());
  104
+    $this->bag->mock->shouldReceive(array('_read' => array()));
  105
+    $this->bag->mock->shouldReceive('_write')->with(array('quux' => 'zoinks', 'foo/bar' => 'baz'))->once();
  106
+
  107
+    $this->bag->replace(array('quux' => 'zoinks', 'foo' => array('bar' => 'baz')));
  108
+  }
  109
+
  110
+  public function testHasFlat()
  111
+  {
  112
+    $this->bag->mock->shouldReceive(array('_read' => array('baz' => 'bar')));
  113
+
  114
+    $this->assertTrue($this->bag->has('baz'));
  115
+  }
  116
+
  117
+  public function testHasDeep()
  118
+  {
  119
+    $this->bag->mock->shouldReceive(array('_read' => array('baz/foo' => 'bar')));
  120
+
  121
+    $this->assertTrue($this->bag->has('baz'));
  122
+  }
  123
+
  124
+  public function testHasWithSlash()
  125
+  {
  126
+    $this->bag->mock->shouldReceive(array('_read' => array('foo%>baz' => 'bar')));
  127
+
  128
+    $this->assertTrue($this->bag->has('foo/baz'));
  129
+  }
  130
+
  131
+  public function testHasnt()
  132
+  {
  133
+    $this->bag->mock->shouldReceive(array('_read' => array('baz/foo' => 'bar')));
  134
+
  135
+    $this->assertFalse($this->bag->has('foo'));
  136
+  }
  137
+
  138
+  public function testRemove()
  139
+  {
  140
+    $this->bag->mock->shouldReceive(array('_read' => array('foo/bar' => 'baz', 'foo/baz' => 'bar', 'quux' => 'zoinks')));
  141
+    $this->bag->mock->shouldReceive('_write')->with(array('quux' => 'zoinks'))->once();
  142
+
  143
+    $this->bag->remove('foo');
  144
+  }
  145
+
  146
+  public function tearDown()
  147
+  {
  148
+    M::close();
  149
+  }
  150
+}
  151
+
  152
+/**
  153
+* 
  154
+*/
  155
+class FlatteningParameterBagMockProxy extends FlatteningParameterBag
  156
+{
  157
+  public $mock;
  158
+
  159
+  public function __construct($mock)
  160
+  {
  161
+    $this->mock = $mock;
  162
+  }
  163
+
  164
+  protected function _read()
  165
+  {
  166
+    return $this->mock->_read();
  167
+  }
  168
+
  169
+  protected function _write(array $data)