Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Finishing the Battle

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Head to the controller. We've got this form flow mastered! Step 1: create a new BattleModel object: $battleModel = new BattleModel():

... lines 1 - 11
class BattleController extends BaseController
{
/**
* @Route("/api/battles")
* @Method("POST")
*/
public function newAction(Request $request)
{
$battleModel = new BattleModel();
... lines 21 - 34
}
}

Step 2: create the form: $form = $this->createForm() with BattleType::class:

... lines 1 - 5
use AppBundle\Form\BattleType;
... lines 7 - 11
class BattleController extends BaseController
{
... lines 14 - 17
public function newAction(Request $request)
{
$battleModel = new BattleModel();
$form = $this->createForm(BattleType::class, $battleModel);
... lines 22 - 34
}
}

On my version of PhpStorm, I need to go back and re-type the e to trigger auto-completion so that the use statement is added above.

For the second argument to createForm: pass it $battleModel.

Step 3: Use $this->processForm():

... lines 1 - 11
class BattleController extends BaseController
{
... lines 14 - 17
public function newAction(Request $request)
{
$battleModel = new BattleModel();
$form = $this->createForm(BattleType::class, $battleModel);
$this->processForm($request, $form);
... lines 23 - 34
}
}

Remember, this is a method we added in BaseController:

... lines 1 - 19
abstract class BaseController extends Controller
{
... lines 22 - 142
protected function processForm(Request $request, FormInterface $form)
{
$data = json_decode($request->getContent(), true);
if ($data === null) {
$apiProblem = new ApiProblem(400, ApiProblem::TYPE_INVALID_REQUEST_BODY_FORMAT);
throw new ApiProblemException($apiProblem);
}
$clearMissing = $request->getMethod() != 'PATCH';
$form->submit($data, $clearMissing);
}
... lines 155 - 185
}

it decodes the Request body and submits it into the form, which is what we do on every endpoint that processes data.

Type-hint the Request argument for the controller and pass this to processForm().

If the form is not valid, we need to send back errors. Use another method from earlier: $this->throwApiProblemValidationException() and pass it the $form object:

... lines 1 - 11
class BattleController extends BaseController
{
... lines 14 - 17
public function newAction(Request $request)
{
$battleModel = new BattleModel();
$form = $this->createForm(BattleType::class, $battleModel);
$this->processForm($request, $form);
if (!$form->isValid()) {
$this->throwApiProblemValidationException($form);
}
... lines 27 - 34
}
}

This will grab the validation errors off and create that response.

At this point, we have a BattleModel object that's populated with the Programmer and Project objects sent in the request. To create the battle, we need to use the BattleManager. Do that with $this->getBattleManager() - that's just a shortcut to get the service - ->battle() and pass it $battleModel->getProgrammer() and $battleModel->getProject():

... lines 1 - 11
class BattleController extends BaseController
{
... lines 14 - 17
public function newAction(Request $request)
{
$battleModel = new BattleModel();
$form = $this->createForm(BattleType::class, $battleModel);
$this->processForm($request, $form);
if (!$form->isValid()) {
$this->throwApiProblemValidationException($form);
}
$battle = $this->getBattleManager()->battle(
$battleModel->getProgrammer(),
$battleModel->getProject()
);
... lines 32 - 34
}
}

Put a little $battle = in the beginning of all of this to get the new Battle object. Perfect!

Now that the battle has been heroically fought, let's send back the gory details. Use return $this->createApiResponse() and pass it $battle and the 201 status code:

... lines 1 - 11
class BattleController extends BaseController
{
... lines 14 - 17
public function newAction(Request $request)
{
$battleModel = new BattleModel();
$form = $this->createForm(BattleType::class, $battleModel);
$this->processForm($request, $form);
if (!$form->isValid()) {
$this->throwApiProblemValidationException($form);
}
$battle = $this->getBattleManager()->battle(
$battleModel->getProgrammer(),
$battleModel->getProject()
);
// todo - set Location header
return $this->createApiResponse($battle, 201);
}
}

We aren't setting a Location header yet, so let's at least add a todo for that.

We are done! Controller, model and form: these are the only pieces we need to create a robust endpoint. Try the test:

./vendor/bin/phpunit --filter testPOSTCreateBattle

We are in prime battling shape.

Now, let's complicate things and learn how to really take control of every field in our endpoint. And, learn more about relations.

Leave a comment!

6
Login or Register to join the conversation
Agnes Avatar

If anyone is getting an error running this test with the downloaded code, be aware that the jwt_key_pass_phrase needs to be set to happyapi from the last course.

Reply

Thanks Agnes! I just updated the parameters.yml.dist file to use this as the default value... just to help people a bit more. Thanks for the tip on this!

Reply
Default user avatar

Hi,
I'm getting an error, when running `./vendor/bin/phpunit --filter testPOSTCreateBattle`:

There was 1 error:

1) AppBundle\Controller\Api\BattleControllerTest::testPOSTCreateBattle

Doctrine\DBAL\Exception\InvalidFieldNameException: An exception occurred while executing 'INSERT INTO battle_project (name, difficulty_level) VALUES (?, ?)' with params ["my_project", 4]:

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'difficulty_level' in 'field list'

the column is certainly there. I have even dropped the database and done the following:
./bin/console doctrine:database:create
./bin/console doctrine:schema:create
and get the same error.

What could be going on?

Reply
Default user avatar

When I run the query manually, it works fine:
INSERT INTO battle_project (name, difficulty_level) VALUES ("my_project",4)

Reply
Vladimir Z. Avatar
Vladimir Z. Avatar Vladimir Z. | Vlad | posted 4 years ago

Sorry, Ryan, I figured it out! I was using the test config (config_test.yml), which had a different database name (suffixed with '_test').
Please delete my comments.
Thank you!

Reply

Ha, nice detective work Vlad! It's probably better you figured it out - you won't forget about this in the future :)

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony. The concepts of Hypermedia & HATEOAS are still valid. But I recommend using API Platform in modern Symfony apps.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.0.*", // v3.0.3
        "doctrine/orm": "^2.5", // v2.5.4
        "doctrine/doctrine-bundle": "^1.6", // 1.6.2
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // v2.10.0
        "sensio/distribution-bundle": "^5.0", // v5.0.4
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.14
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.2
        "jms/serializer-bundle": "^1.1.0", // 1.1.0
        "white-october/pagerfanta-bundle": "^1.0", // v1.0.5
        "lexik/jwt-authentication-bundle": "^1.4", // v1.4.3
        "willdurand/hateoas-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.6
        "symfony/phpunit-bridge": "^3.0", // v3.0.3
        "behat/behat": "~3.1@dev", // dev-master
        "behat/mink-extension": "~2.2.0", // v2.2
        "behat/mink-goutte-driver": "~1.2.0", // v1.2.1
        "behat/mink-selenium2-driver": "~1.3.0", // v1.3.1
        "phpunit/phpunit": "~4.6.0", // 4.6.10
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}