├── .gitignore ├── Resources └── config │ └── services.yml ├── OutlandishAcfOowpBundle.php ├── Model └── Acf.php ├── composer.json ├── Exception ├── NotRepeaterException.php └── InvalidRepeaterFieldNameException.php ├── spec └── Outlandish │ └── SiteBundle │ ├── OutlandishSiteBundleSpec.php │ └── Repository │ └── AcfSpec.php ├── DependencyInjection └── OutlandishAcfOowpExtension.php ├── README.md ├── Repository └── Acf.php └── Wrapper └── Acf.php /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | outlandish_acf.wrapper.acf: 3 | class: Outlandish\AcfOowpBundle\Wrapper\Acf 4 | 5 | outlandish_acf.repository.acf: 6 | class: Outlandish\AcfOowpBundle\Repository\Acf 7 | arguments: [@outlandish_acf.wrapper.acf] -------------------------------------------------------------------------------- /OutlandishAcfOowpBundle.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('Outlandish\SiteBundle\OutlandishAcfOowpBundle'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /DependencyInjection/OutlandishAcfOowpExtension.php: -------------------------------------------------------------------------------- 1 | load('services.yml'); 24 | } 25 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Acf Oowp Bundle 2 | ======================== 3 | 4 | Symfony 2 Bundle to use with the Wordpress enabled Symfony 2 using the [RoutemasterBundle](https://github.com/outlandishideas/RoutemasterBundle) 5 | when using the [Advanced Custom Fields plugin](http://www.advancedcustomfields.com/). 6 | 7 | It wraps the Advanced Custom Field plugin functions within a service, allowing you inject these functions as a dependency for other services. 8 | Additionally it provides a Repository service that can used to save simple Models in the database that can be edited on the backend using 9 | custom fields. 10 | 11 | ## Features 12 | 13 | * Wraps Advanced Custom Field plugin functions within a service, to make them testable and injectable. 14 | * Store simple Models using Advanced Custom Fields with the Acf Repository service (Models must implement the Acf Model interface). 15 | 16 | ## Installation 17 | 18 | * Add the github repository `https://github.com/outlandishideas/AcfOowpBundle.git` to the repositories section of your composer.json file 19 | * Run `composer require outlandish/acf-oowp-bundle` (assuming [Composer](http://getcomposer.org/download/) is installed globally). 20 | 21 | ## Using Acf to store your models 22 | 23 | Using Repeater Fields or Flexible Content fields, it is possible to store representations of a model using ACF. 24 | The sub fields of a repeater field would store the different fields of an object, and each row would represent a new model. 25 | 26 | This allows for the storing of simple models different to storing them as a custom post type, while providing 27 | the user a way of adding, deleting and updating them through the Wordpress interface. 28 | 29 | * Create a new model with a number of fields. These fields must be possible to represent in some form using Acf fields. 30 | * Create the repeater field in your Wordpress instance. 31 | * Use the `Outlandish\AcfOowpBundle\Repository\Acf` to fetch, delete, update and create your models into Acf field.á -------------------------------------------------------------------------------- /Repository/Acf.php: -------------------------------------------------------------------------------- 1 | acf = $acf; 26 | } 27 | 28 | public function getField($fieldKey, $postId = 'options', $format = true) 29 | { 30 | $field = $this->acf->getField($fieldKey, $postId, $format); 31 | 32 | $this->validateRepeaterField($field); 33 | 34 | return $field; 35 | } 36 | 37 | /** 38 | * @param string $fieldKey 39 | * @param string $idKey 40 | * @param mixed $id 41 | * @param string $postId 42 | * @param bool $format 43 | * 44 | * @return null 45 | */ 46 | public function fetch($fieldKey, $idKey, $id, $postId = 'options', $format = true) 47 | { 48 | $field = $this->getField($fieldKey, $postId, $format); 49 | 50 | return $this->fetchRow($idKey, $id, $field); 51 | } 52 | 53 | /** 54 | * @param mixed $field 55 | * 56 | * @return bool 57 | */ 58 | protected function isRepeaterField($field) 59 | { 60 | return is_array($field); 61 | } 62 | 63 | /** 64 | * @param array $field 65 | * 66 | * @return bool 67 | */ 68 | protected function hasRows(array $field) 69 | { 70 | return !empty($field); 71 | } 72 | 73 | /** 74 | * @param string $idKey 75 | * @param array $field 76 | * 77 | * @throws InvalidRepeaterFieldNameException 78 | */ 79 | protected function idKeyExists($idKey, array $field) 80 | { 81 | if (!array_key_exists($idKey, $field[0])) { 82 | throw new InvalidRepeaterFieldNameException(); 83 | } 84 | } 85 | 86 | /** 87 | * @param string $field 88 | * 89 | * @throws NotRepeaterException 90 | */ 91 | protected function validateRepeaterField($field) 92 | { 93 | if (!$this->isRepeaterField($field)) { 94 | throw new NotRepeaterException(); 95 | } 96 | } 97 | 98 | /** 99 | * @param string $fieldKey 100 | * @param string $idKey 101 | * @param mixed $id 102 | * @param string|int $postID 103 | * 104 | * @throws InvalidRepeaterFieldNameException 105 | * @throws NotRepeaterException 106 | */ 107 | public function delete($fieldKey, $idKey, $id, $postID = 'options') 108 | { 109 | $field = $this->acf->getField($fieldKey, $postID); 110 | 111 | $this->validateRepeaterField($field); 112 | 113 | if ($this->hasRows($field)) { 114 | $this->idKeyExists($idKey, $field); 115 | 116 | $newValue = $this->filterFieldRows($idKey, $id, $field); 117 | 118 | $this->acf->updateField($fieldKey, $newValue, $postID); 119 | } 120 | 121 | 122 | } 123 | 124 | /** 125 | * @param string $fieldKey 126 | * @param AcfModel $model 127 | * @param int|string $postId 128 | * 129 | * @return bool 130 | */ 131 | public function create($fieldKey, AcfModel $model, $postId) 132 | { 133 | $field = $this->acf->getField($fieldKey, $postId); 134 | 135 | $field[] = $model->toArray(); 136 | 137 | return $this->acf->updateField($fieldKey, $field, $postId); 138 | } 139 | 140 | /** 141 | * @param string $fieldKey 142 | * @param string $idKey 143 | * @param AcfModel $model 144 | * @param string $postID 145 | * 146 | * @throws InvalidRepeaterFieldNameException 147 | * @throws NotRepeaterException 148 | */ 149 | public function update($fieldKey, $idKey, AcfModel $model, $postID = 'options') 150 | { 151 | $field = $this->getField($fieldKey, $postID); 152 | 153 | if ($this->hasRows($field)) { 154 | $this->idKeyExists($idKey, $field); 155 | $newRow = $model->toArray(); 156 | 157 | foreach ($field as &$row) { 158 | if ($row[$idKey] == $newRow[$idKey]) { 159 | $row = $newRow; 160 | break; 161 | } 162 | } 163 | } 164 | 165 | $this->acf->updateField($fieldKey, $field, $postID); 166 | } 167 | 168 | /** 169 | * @param $idKey 170 | * @param $id 171 | * @param $field 172 | * @return array 173 | */ 174 | protected function filterFieldRows($idKey, $id, $field) 175 | { 176 | return array_filter($field, function ($row) use ($idKey, $id) { 177 | return $row[$idKey] != $id; 178 | }); 179 | } 180 | 181 | /** 182 | * @param $idKey 183 | * @param $id 184 | * @param $field 185 | * 186 | * @return null|array 187 | */ 188 | protected function fetchRow($idKey, $id, $field) 189 | { 190 | if ($this->hasRows($field)) { 191 | $this->idKeyExists($idKey, $field); 192 | 193 | foreach ($field as $row) { 194 | if ($row[$idKey] == $id) { 195 | return $row; 196 | } 197 | } 198 | } 199 | 200 | return null; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /spec/Outlandish/SiteBundle/Repository/AcfSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($acf); 16 | } 17 | 18 | function it_is_initializable() 19 | { 20 | $this->shouldHaveType('Outlandish\SiteBundle\Acf\AcfRepository'); 21 | } 22 | 23 | function it_filters_the_results_of_a_field_by_id_key_and_id(AcfWrapper $acf) 24 | { 25 | $fieldKey = 'field_key'; 26 | $postId = 12; 27 | $idKey = 'id'; 28 | 29 | $row1 = ['id' => 'example']; 30 | $row2 = ['id' => 'example2']; 31 | 32 | $call = $acf->getField($fieldKey, $postId, true); 33 | $call->shouldBeCalled(); 34 | $call->willReturn([$row1, $row2]); 35 | 36 | $this->fetch($fieldKey, $idKey, 'example', $postId)->shouldReturn($row1); 37 | } 38 | 39 | function it_defaults_the_post_id_to_options(AcfWrapper $acf) 40 | { 41 | $call = $acf->getField(Argument::any(), 'options', true); 42 | $call->shouldBeCalled(); 43 | $call->willReturn([]); 44 | 45 | $this->fetch(Argument::any(), Argument::any(), Argument::any()); 46 | } 47 | 48 | function it_defaults_the_format_to_true(AcfWrapper $acf) 49 | { 50 | $call = $acf->getField(Argument::any(), Argument::any(), true); 51 | $call->shouldBeCalled(); 52 | $call->willReturn([]); 53 | 54 | $this->fetch(Argument::any(), Argument::any(), Argument::any()); 55 | } 56 | 57 | function it_returns_null_if_the_acf_field_has_no_rows(AcfWrapper $acf) 58 | { 59 | $call = $acf->getField(Argument::cetera()); 60 | $call->willReturn([]); 61 | 62 | $this->fetch(Argument::any(), Argument::any(), Argument::any())->shouldReturn(null); 63 | } 64 | 65 | function it_throws_an_exception_if_the_field_is_not_a_repeater(AcfWrapper $acf, Model $model) 66 | { 67 | $call = $acf->getField(Argument::cetera()); 68 | $call->willReturn('a string'); 69 | 70 | $this->shouldThrow('Outlandish\SiteBundle\Acf\Exception\NotRepeaterException') 71 | ->duringFetch(Argument::any(), Argument::any(), Argument::any()); 72 | $this->shouldThrow('Outlandish\SiteBundle\Acf\Exception\NotRepeaterException') 73 | ->duringDelete(Argument::any(), Argument::any(), Argument::any()); 74 | $this->shouldThrow('Outlandish\SiteBundle\Acf\Exception\NotRepeaterException') 75 | ->duringUpdate(Argument::any(), 'name', $model); 76 | } 77 | 78 | function it_throws_an_exception_if_the_fields_rows_do_not_contain_a_field_with_name_that_equals_id_key(AcfWrapper $acf, Model $model) 79 | { 80 | $row = ['bad_id' => null]; 81 | $call = $acf->getField(Argument::cetera()); 82 | $call->willReturn([$row]); 83 | 84 | $this->shouldThrow('Outlandish\SiteBundle\Acf\Exception\InvalidRepeaterFieldNameException') 85 | ->duringFetch(Argument::any(), 'id', Argument::any()); 86 | $this->shouldThrow('Outlandish\SiteBundle\Acf\Exception\InvalidRepeaterFieldNameException') 87 | ->duringDelete(Argument::any(), 'id', 1); 88 | $this->shouldThrow('Outlandish\SiteBundle\Acf\Exception\InvalidRepeaterFieldNameException') 89 | ->duringUpdate(Argument::any(), 'id', $model); 90 | 91 | } 92 | 93 | function it_returns_null_if_id_does_not_exist_in_rows_of_field(AcfWrapper $acf) 94 | { 95 | $row1 = ['id' => 1]; 96 | $row2 = ['id' => 2]; 97 | 98 | $call = $acf->getField(Argument::cetera()); 99 | $call->willReturn([$row1, $row2]); 100 | 101 | $this->fetch(Argument::any(), 'id', 3)->shouldReturn(null); 102 | } 103 | 104 | function it_deletes_a_row_from_a_field(AcfWrapper $acf) 105 | { 106 | $row1 = ['id' => 1]; 107 | $row2 = ['id' => 2]; 108 | 109 | $call = $acf->getField(Argument::any(), Argument::any()); 110 | $call->shouldBeCalled(); 111 | $call->willReturn([$row1, $row2]); 112 | 113 | $acf->updateField(Argument::any(), [$row1], Argument::any())->shouldBeCalled(); 114 | 115 | $this->delete(Argument::any(), 'id', 2, Argument::any()); 116 | } 117 | 118 | function it_defaults_post_id_to_options(AcfWrapper $acf) 119 | { 120 | $call = $acf->getField(Argument::any(), 'options'); 121 | $call->shouldBeCalled(); 122 | $call->willReturn([]); 123 | 124 | $this->delete(Argument::any(), Argument::any(), Argument::any()); 125 | } 126 | 127 | function it_updates_a_row_within_a_repeater_field(AcfWrapper $acf, Model $model) 128 | { 129 | $row = ['name' => 'name', 'label' => 'Label']; 130 | $toArray = ['name' => 'name', 'label' => 'New Label']; 131 | 132 | $call = $model->toArray(); 133 | $call->shouldBeCalled(); 134 | $call->willReturn($toArray); 135 | 136 | $call = $acf->getField(Argument::cetera()); 137 | $call->willReturn([$row]); 138 | 139 | $acf->updateField(Argument::any(), [$toArray], Argument::any())->shouldBeCalled(); 140 | 141 | $this->update(Argument::any(), 'name', $model, Argument::any()); 142 | } 143 | 144 | function it_sets_the_default_post_id_as_options_when_updating(AcfWrapper $acf, Model $model) 145 | { 146 | $call = $acf->getField(Argument::any(), 'options'); 147 | $call->shouldBeCalled(); 148 | $call->willReturn([]); 149 | 150 | $acf->updateField(Argument::any(), [], 'options')->shouldBeCalled(); 151 | 152 | $this->update(Argument::any(), Argument::any(), $model); 153 | } 154 | 155 | function it_creates_a_new_article_type(AcfWrapper $acf, Model $model) 156 | { 157 | $name = 'name'; 158 | $toArray = ['name' => $name]; 159 | 160 | $call = $model->toArray(); 161 | $call->shouldBeCalled(); 162 | $call->willReturn($toArray); 163 | 164 | $call = $acf->getField(Argument::any(), 'options'); 165 | $call->shouldBeCalled(); 166 | $call->willReturn([]); 167 | 168 | $call = $acf->updateField(Argument::any(), [$toArray], 'options'); 169 | $call->shouldBeCalled(); 170 | $call->willReturn(true); 171 | 172 | $this->create(Argument::any(), $model, 'options')->shouldReturn(true); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Wrapper/Acf.php: -------------------------------------------------------------------------------- 1 |