Jelajahi Sumber

Lesson 19 step 2: CRUD support on PHP backend. Working from Axios.

Frederic G. MARAND 6 tahun lalu
induk
melakukan
dfe95afbab

+ 75 - 126
.idea/workspace.xml

@@ -2,30 +2,12 @@
 <project version="4">
   <component name="ChangeListManager">
     <list default="true" id="0f813586-48e2-4acf-8923-221617ab434f" name="Default" comment="">
-      <change type="NEW" beforePath="" afterPath="$PROJECT_DIR$/lesson19/code.js" />
-      <change type="NEW" beforePath="" afterPath="$PROJECT_DIR$/lesson19/composer.json" />
-      <change type="NEW" beforePath="" afterPath="$PROJECT_DIR$/lesson19/composer.lock" />
-      <change type="NEW" beforePath="" afterPath="$PROJECT_DIR$/lesson19/index.html" />
-      <change type="NEW" beforePath="" afterPath="$PROJECT_DIR$/lesson19/index.php" />
-      <change type="NEW" beforePath="" afterPath="$PROJECT_DIR$/lesson19/skills.json" />
-      <change type="NEW" beforePath="" afterPath="$PROJECT_DIR$/lesson19/src/ProjectManager.php" />
-      <change type="NEW" beforePath="" afterPath="$PROJECT_DIR$/lesson19/styles.css" />
-      <change type="NEW" beforePath="" afterPath="$PROJECT_DIR$/lib/bulma-0.2.3.css" />
-      <change type="NEW" beforePath="" afterPath="$PROJECT_DIR$/lib/bulma-0.5.0.css" />
-      <change type="DELETED" beforePath="$PROJECT_DIR$/.idea/inspectionProfiles/profiles_settings.xml" afterPath="" />
-      <change type="MOVED" beforePath="$PROJECT_DIR$/lesson18/axios.min.js" afterPath="$PROJECT_DIR$/lib/axios-0.16.2.min.js" />
-      <change type="MODIFICATION" beforePath="$PROJECT_DIR$/.gitignore" afterPath="$PROJECT_DIR$/.gitignore" />
+      <change type="NEW" beforePath="" afterPath="$PROJECT_DIR$/lesson19/src/ContainerInjectionInterface.php" />
+      <change type="NEW" beforePath="" afterPath="$PROJECT_DIR$/lesson19/src/Project.php" />
+      <change type="NEW" beforePath="" afterPath="$PROJECT_DIR$/lesson19/src/ProjectStore.php" />
       <change type="MODIFICATION" beforePath="$PROJECT_DIR$/.idea/workspace.xml" afterPath="$PROJECT_DIR$/.idea/workspace.xml" />
-      <change type="MODIFICATION" beforePath="$PROJECT_DIR$/lesson09/index.html" afterPath="$PROJECT_DIR$/lesson09/index.html" />
-      <change type="MODIFICATION" beforePath="$PROJECT_DIR$/lesson10/index.html" afterPath="$PROJECT_DIR$/lesson10/index.html" />
-      <change type="MODIFICATION" beforePath="$PROJECT_DIR$/lesson11/index.html" afterPath="$PROJECT_DIR$/lesson11/index.html" />
-      <change type="MODIFICATION" beforePath="$PROJECT_DIR$/lesson12/index.html" afterPath="$PROJECT_DIR$/lesson12/index.html" />
-      <change type="MODIFICATION" beforePath="$PROJECT_DIR$/lesson13/index.html" afterPath="$PROJECT_DIR$/lesson13/index.html" />
-      <change type="MODIFICATION" beforePath="$PROJECT_DIR$/lesson14/index.html" afterPath="$PROJECT_DIR$/lesson14/index.html" />
-      <change type="MODIFICATION" beforePath="$PROJECT_DIR$/lesson15/index.html" afterPath="$PROJECT_DIR$/lesson15/index.html" />
-      <change type="MODIFICATION" beforePath="$PROJECT_DIR$/lesson16/index.html" afterPath="$PROJECT_DIR$/lesson16/index.html" />
-      <change type="MODIFICATION" beforePath="$PROJECT_DIR$/lesson17/index.html" afterPath="$PROJECT_DIR$/lesson17/index.html" />
-      <change type="MODIFICATION" beforePath="$PROJECT_DIR$/lesson18/index.html" afterPath="$PROJECT_DIR$/lesson18/index.html" />
+      <change type="MODIFICATION" beforePath="$PROJECT_DIR$/lesson19/index.php" afterPath="$PROJECT_DIR$/lesson19/index.php" />
+      <change type="MODIFICATION" beforePath="$PROJECT_DIR$/lesson19/src/ProjectManager.php" afterPath="$PROJECT_DIR$/lesson19/src/ProjectManager.php" />
     </list>
     <option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
     <option name="TRACKING_ENABLED" value="true" />
@@ -38,38 +20,7 @@
     <executablePath>composer</executablePath>
   </component>
   <component name="FileEditorManager">
-    <leaf>
-      <file leaf-file-name="index.php" pinned="false" current-in-tab="true">
-        <entry file="file://$PROJECT_DIR$/lesson19/index.php">
-          <provider selected="true" editor-type-id="text-editor">
-            <state relative-caret-position="270">
-              <caret line="18" column="0" lean-forward="false" selection-start-line="18" selection-start-column="0" selection-end-line="18" selection-end-column="0" />
-              <folding />
-            </state>
-          </provider>
-        </entry>
-      </file>
-      <file leaf-file-name="ProjectManager.php" pinned="false" current-in-tab="false">
-        <entry file="file://$PROJECT_DIR$/lesson19/src/ProjectManager.php">
-          <provider selected="true" editor-type-id="text-editor">
-            <state relative-caret-position="75">
-              <caret line="5" column="0" lean-forward="false" selection-start-line="5" selection-start-column="0" selection-end-line="5" selection-end-column="0" />
-              <folding />
-            </state>
-          </provider>
-        </entry>
-      </file>
-      <file leaf-file-name="code.js" pinned="false" current-in-tab="false">
-        <entry file="file://$PROJECT_DIR$/lesson19/code.js">
-          <provider selected="true" editor-type-id="text-editor">
-            <state relative-caret-position="285">
-              <caret line="19" column="33" lean-forward="false" selection-start-line="19" selection-start-column="33" selection-end-line="19" selection-end-column="33" />
-              <folding />
-            </state>
-          </provider>
-        </entry>
-      </file>
-    </leaf>
+    <leaf />
   </component>
   <component name="FindInProjectRecents">
     <findStrings>
@@ -108,9 +59,6 @@
   <component name="IdeDocumentHistory">
     <option name="CHANGED_PATHS">
       <list>
-        <option value="$PROJECT_DIR$/lesson6/code.js" />
-        <option value="$PROJECT_DIR$/lesson6/index.html" />
-        <option value="$PROJECT_DIR$/lesson7/index.html" />
         <option value="$PROJECT_DIR$/lesson7/code.js" />
         <option value="$PROJECT_DIR$/lesson8/index.html" />
         <option value="$PROJECT_DIR$/.eslintrc.json" />
@@ -156,9 +104,12 @@
         <option value="$PROJECT_DIR$/lesson19/Projects.php" />
         <option value="$PROJECT_DIR$/lesson19/ProjectManager.php" />
         <option value="$PROJECT_DIR$/lesson19/composer.json" />
-        <option value="$PROJECT_DIR$/lesson19/index.php" />
-        <option value="$PROJECT_DIR$/lesson19/src/ProjectManager.php" />
         <option value="$PROJECT_DIR$/lesson19/code.js" />
+        <option value="$PROJECT_DIR$/lesson19/src/ContainerInjectionInterface.php" />
+        <option value="$PROJECT_DIR$/lesson19/src/ProjectManager.php" />
+        <option value="$PROJECT_DIR$/lesson19/index.php" />
+        <option value="$PROJECT_DIR$/lesson19/src/Project.php" />
+        <option value="$PROJECT_DIR$/lesson19/src/ProjectStore.php" />
       </list>
     </option>
   </component>
@@ -415,12 +366,12 @@
       <workItem from="1500666240713" duration="643000" />
       <workItem from="1501442497993" duration="54000" />
       <workItem from="1501442560946" duration="143000" />
-      <workItem from="1501834423911" duration="7472000" />
+      <workItem from="1501834423911" duration="10626000" />
     </task>
     <servers />
   </component>
   <component name="TimeTrackingManager">
-    <option name="totallyTimeSpent" value="31861000" />
+    <option name="totallyTimeSpent" value="35015000" />
   </component>
   <component name="ToolWindowManager">
     <frame x="0" y="23" width="1436" height="877" extended-state="6" />
@@ -431,13 +382,13 @@
       <window_info id="Run" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="6" side_tool="false" content_ui="tabs" />
       <window_info id="Mongo Explorer" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
       <window_info id="Terminal" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.3299363" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
-      <window_info id="Project" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.22812052" sideWeight="0.5" order="5" side_tool="false" content_ui="combo" />
+      <window_info id="Project" active="true" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.22812052" sideWeight="0.5" order="5" side_tool="false" content_ui="combo" />
       <window_info id="Docker" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="11" side_tool="false" content_ui="tabs" />
       <window_info id="Database" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="0" side_tool="false" content_ui="tabs" />
-      <window_info id="Find" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="5" side_tool="false" content_ui="tabs" />
+      <window_info id="Find" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.32992327" sideWeight="0.5" order="5" side_tool="false" content_ui="tabs" />
       <window_info id="Structure" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.15566714" sideWeight="0.5" order="1" side_tool="false" content_ui="tabs" />
       <window_info id="Favorites" active="false" anchor="left" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="0" side_tool="true" content_ui="tabs" />
-      <window_info id="Debug" active="true" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.39897698" sideWeight="0.5" order="7" side_tool="false" content_ui="tabs" />
+      <window_info id="Debug" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="true" show_stripe_button="true" weight="0.39897698" sideWeight="0.5" order="7" side_tool="false" content_ui="tabs" />
       <window_info id="Cvs" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.25" sideWeight="0.5" order="8" side_tool="false" content_ui="tabs" />
       <window_info id="Message" active="false" anchor="bottom" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.33" sideWeight="0.5" order="4" side_tool="false" content_ui="tabs" />
       <window_info id="Commander" active="false" anchor="right" auto_hide="false" internal_type="DOCKED" type="DOCKED" visible="false" show_stripe_button="true" weight="0.4" sideWeight="0.5" order="2" side_tool="false" content_ui="tabs" />
@@ -455,56 +406,14 @@
   </component>
   <component name="XDebuggerManager">
     <breakpoint-manager>
-      <breakpoints>
-        <line-breakpoint enabled="true" type="php">
-          <url>file://$PROJECT_DIR$/lesson19/index.php</url>
-          <line>17</line>
-          <option name="timeStamp" value="4" />
-        </line-breakpoint>
-      </breakpoints>
       <breakpoints-dialog>
         <breakpoints-dialog />
       </breakpoints-dialog>
-      <option name="time" value="7" />
+      <option name="time" value="9" />
     </breakpoint-manager>
     <watches-manager />
   </component>
   <component name="editorHistoryManager">
-    <entry file="file://$PROJECT_DIR$/lesson17/lesson/package-lock.json">
-      <provider selected="true" editor-type-id="text-editor">
-        <state relative-caret-position="318">
-          <caret line="25" column="30" lean-forward="true" selection-start-line="25" selection-start-column="30" selection-end-line="25" selection-end-column="30" />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/lesson17/lesson/package.json">
-      <provider selected="true" editor-type-id="text-editor">
-        <state relative-caret-position="0">
-          <caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/lesson17/lesson/src/components/Counter.vue">
-      <provider selected="true" editor-type-id="text-editor">
-        <state relative-caret-position="270">
-          <caret line="18" column="9" lean-forward="true" selection-start-line="18" selection-start-column="9" selection-end-line="18" selection-end-column="9" />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/lesson17/lesson/src/App.vue">
-      <provider selected="true" editor-type-id="text-editor">
-        <state relative-caret-position="195">
-          <caret line="13" column="15" lean-forward="false" selection-start-line="13" selection-start-column="15" selection-end-line="13" selection-end-column="15" />
-        </state>
-      </provider>
-    </entry>
-    <entry file="file://$PROJECT_DIR$/lesson17/lesson/src/main.js">
-      <provider selected="true" editor-type-id="text-editor">
-        <state relative-caret-position="0">
-          <caret line="0" column="0" lean-forward="false" selection-start-line="0" selection-start-column="0" selection-end-line="0" selection-end-column="0" />
-        </state>
-      </provider>
-    </entry>
     <entry file="file://$PROJECT_DIR$/lesson18/axios.min.js">
       <provider selected="true" editor-type-id="text-editor">
         <state relative-caret-position="0">
@@ -750,14 +659,6 @@
         </state>
       </provider>
     </entry>
-    <entry file="file://$PROJECT_DIR$/lesson19/vendor/composer/ClassLoader.php">
-      <provider selected="true" editor-type-id="text-editor">
-        <state relative-caret-position="105">
-          <caret line="323" column="0" lean-forward="false" selection-start-line="323" selection-start-column="0" selection-end-line="323" selection-end-column="0" />
-          <folding />
-        </state>
-      </provider>
-    </entry>
     <entry file="file://$PROJECT_DIR$/lesson19/index.html">
       <provider selected="true" editor-type-id="text-editor">
         <state relative-caret-position="285">
@@ -846,34 +747,82 @@
         </state>
       </provider>
     </entry>
+    <entry file="file://$PROJECT_DIR$/lesson19/vendor/psr/container/src/ContainerInterface.php">
+      <provider selected="true" editor-type-id="text-editor">
+        <state relative-caret-position="147">
+          <caret line="10" column="10" lean-forward="false" selection-start-line="10" selection-start-column="10" selection-end-line="10" selection-end-column="10" />
+          <folding />
+        </state>
+      </provider>
+    </entry>
+    <entry file="file://$PROJECT_DIR$/lesson19/src/ContainerInjectionInterface.php">
+      <provider selected="true" editor-type-id="text-editor">
+        <state relative-caret-position="90">
+          <caret line="6" column="36" lean-forward="false" selection-start-line="6" selection-start-column="36" selection-end-line="6" selection-end-column="36" />
+          <folding />
+        </state>
+      </provider>
+    </entry>
+    <entry file="file://$PROJECT_DIR$/lesson19/vendor/composer/ClassLoader.php">
+      <provider selected="true" editor-type-id="text-editor">
+        <state relative-caret-position="105">
+          <caret line="323" column="0" lean-forward="false" selection-start-line="323" selection-start-column="0" selection-end-line="323" selection-end-column="0" />
+          <folding />
+        </state>
+      </provider>
+    </entry>
+    <entry file="file://$PROJECT_DIR$/lesson19/src/Project.php">
+      <provider selected="true" editor-type-id="text-editor">
+        <state relative-caret-position="210">
+          <caret line="14" column="0" lean-forward="false" selection-start-line="14" selection-start-column="0" selection-end-line="14" selection-end-column="0" />
+          <folding />
+        </state>
+      </provider>
+    </entry>
     <entry file="file://$PROJECT_DIR$/lesson19/vendor/symfony/http-kernel/HttpKernel.php">
       <provider selected="true" editor-type-id="text-editor">
-        <state relative-caret-position="222">
-          <caret line="160" column="47" lean-forward="false" selection-start-line="160" selection-start-column="47" selection-end-line="160" selection-end-column="47" />
+        <state relative-caret-position="147">
+          <caret line="155" column="0" lean-forward="false" selection-start-line="155" selection-start-column="0" selection-end-line="155" selection-end-column="0" />
+          <folding />
+        </state>
+      </provider>
+    </entry>
+    <entry file="file://$PROJECT_DIR$/lesson19/project.json">
+      <provider selected="true" editor-type-id="text-editor">
+        <state relative-caret-position="135">
+          <caret line="9" column="1" lean-forward="true" selection-start-line="9" selection-start-column="1" selection-end-line="9" selection-end-column="1" />
+          <folding />
+        </state>
+      </provider>
+    </entry>
+    <entry file="file://$PROJECT_DIR$/lesson19/index.php">
+      <provider selected="true" editor-type-id="text-editor">
+        <state relative-caret-position="120">
+          <caret line="8" column="0" lean-forward="false" selection-start-line="8" selection-start-column="0" selection-end-line="8" selection-end-column="0" />
           <folding />
         </state>
       </provider>
     </entry>
     <entry file="file://$PROJECT_DIR$/lesson19/src/ProjectManager.php">
       <provider selected="true" editor-type-id="text-editor">
-        <state relative-caret-position="75">
-          <caret line="5" column="0" lean-forward="false" selection-start-line="5" selection-start-column="0" selection-end-line="5" selection-end-column="0" />
+        <state relative-caret-position="225">
+          <caret line="27" column="0" lean-forward="false" selection-start-line="27" selection-start-column="0" selection-end-line="27" selection-end-column="0" />
           <folding />
         </state>
       </provider>
     </entry>
-    <entry file="file://$PROJECT_DIR$/lesson19/code.js">
+    <entry file="file://$PROJECT_DIR$/lesson19/src/ProjectStore.php">
       <provider selected="true" editor-type-id="text-editor">
-        <state relative-caret-position="285">
-          <caret line="19" column="33" lean-forward="false" selection-start-line="19" selection-start-column="33" selection-end-line="19" selection-end-column="33" />
+        <state relative-caret-position="50">
+          <caret line="36" column="53" lean-forward="false" selection-start-line="36" selection-start-column="53" selection-end-line="36" selection-end-column="53" />
           <folding />
         </state>
       </provider>
     </entry>
-    <entry file="file://$PROJECT_DIR$/lesson19/index.php">
+    <entry file="file://$PROJECT_DIR$/lesson19/code.js">
       <provider selected="true" editor-type-id="text-editor">
-        <state relative-caret-position="270">
-          <caret line="18" column="0" lean-forward="false" selection-start-line="18" selection-start-column="0" selection-end-line="18" selection-end-column="0" />
+        <state relative-caret-position="285">
+          <caret line="19" column="33" lean-forward="false" selection-start-line="19" selection-start-column="33" selection-end-line="19" selection-end-column="33" />
           <folding />
         </state>
       </provider>

+ 1 - 0
lesson19/.gitignore

@@ -0,0 +1 @@
+project.json

+ 4 - 2
lesson19/index.php

@@ -1,6 +1,7 @@
 <?php
 
 use Lesson19\ProjectManager;
+use Lesson19\ProjectStore;
 use Pimple\Container as Pimple;
 use Pimple\Psr11\Container;
 use Symfony\Component\HttpFoundation\Request;
@@ -9,10 +10,11 @@ require_once __DIR__ . '/vendor/autoload.php';
 
 $app = new Silex\Application();
 $container = new Container($dic = new Pimple());
-$dic[ProjectManager::NAME] = ProjectManager::create($container);
+$dic[ProjectStore::NAME] = ProjectStore::instantiate($container);
+$dic[ProjectManager::NAME] = ProjectManager::instantiate($container);
 
 /** @var \ProjectManager $projectsManager */
-$projectsManager = $container->get('projects_manager');
+$projectsManager = $container->get(ProjectManager::NAME);
 
 $app->match('/projects', function (Request $req) use($projectsManager) {
   $data = json_decode($req->getContent());

+ 9 - 0
lesson19/src/ContainerInjectionInterface.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace Lesson19;
+
+use Pimple\Psr11\Container;
+
+interface ContainerInjectionInterface {
+  public static function instantiate(Container $container);
+}

+ 16 - 0
lesson19/src/Project.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace Lesson19;
+
+class Project {
+  /** @var string */
+  public $name;
+
+  /** @var string */
+  public $description;
+
+  public function __construct(string $name, string $description) {
+    $this->name = $name;
+    $this->description = $description;
+  }
+}

+ 19 - 6
lesson19/src/ProjectManager.php

@@ -5,17 +5,30 @@ namespace Lesson19;
 use Pimple\Psr11\Container;
 use Symfony\Component\HttpFoundation\Response;
 
-class ProjectManager {
-  const NAME = 'projects_manager';
+class ProjectManager implements ContainerInjectionInterface {
+  const NAME = 'project_manager';
 
-  public function __construct() {
+  /**
+   * @var \Lesson19\ProjectStore
+   */
+  protected $store;
+
+  public function __construct(ProjectStore $store) {
+    $this->store = $store;
   }
 
-  public static function create(Container $container) {
-    return new static();
+  public static function instantiate(Container $container) {
+    $store = $container->get(ProjectStore::NAME);
+    return new static($store);
   }
 
   public function createProject(string $name, string $description) {
-    return new Response("Created", Response::HTTP_CREATED);
+    try {
+      $this->store->create(new Project($name, $description));
+      return new Response("Created", Response::HTTP_CREATED);
+    }
+    catch (\InvalidArgumentException $e) {
+      return new Response($e->getMessage(), Response::HTTP_CONFLICT);
+    }
   }
 }

+ 77 - 0
lesson19/src/ProjectStore.php

@@ -0,0 +1,77 @@
+<?php
+
+namespace Lesson19;
+
+use Pimple\Psr11\Container;
+
+/**
+ * Class ProjectStore
+ *
+ * Yay race conditions :-)
+ *
+ * @package Lesson19
+ */
+class ProjectStore implements ContainerInjectionInterface  {
+  const NAME = 'project_store';
+  const STORE = __DIR__ . '/../project.json';
+
+  protected $data = [];
+
+  public function __construct() {
+    if (!file_exists(self::STORE)) {
+      $this->save();
+    }
+    $this->load();
+  }
+
+  public function __destruct() {
+    $this->save();
+  }
+
+  protected function load() {
+    $raw = file_get_contents(self::STORE);
+    $this->data = json_decode($raw, TRUE);
+  }
+
+  protected function save() {
+    $raw = json_encode($this->data, JSON_PRETTY_PRINT);
+    file_put_contents(self::STORE, $raw);
+  }
+
+  public static function instantiate(Container $container) {
+    return new static();
+  }
+
+  public function create(Project $project) {
+    $name = $project->name;
+    if (isset($this->data[$name])) {
+      throw new \InvalidArgumentException("Project {$name} already exists.");
+    }
+    if (empty($project->name)) {
+      throw new \InvalidArgumentException("Cannot create project without a name.");
+    }
+
+    $this->data[$name] = $project;
+    $this->save();
+  }
+
+  public function retrieve(string $name): Project {
+    if (!isset($this->data[$name])) {
+      throw new \InvalidArgumentException("Trying to retrieve non-existent project $name");
+    }
+    return $this->data[$name];
+  }
+
+  public function update(string $name, Project $new) {
+    // Will throw if project does not exist.
+    $existing = $this->retrieve($name);
+
+    $this->delete($existing);
+    $this->create($new);
+    $this->save();
+  }
+
+  public function delete(Project $project) {
+    unset($this->data[$project->name]);
+  }
+}