themosis / framework

The Themosis framework core.
https://framework.themosis.com/
GNU General Public License v2.0
671 stars 121 forks source link

Front-end forms and custom post types, and meta keys? #205

Closed girvo closed 9 years ago

girvo commented 9 years ago

Hey there,

Quick question. If I have a custom post type, with a Metabox and set of Fields on said custom post type, and I want to have anonymous users submit (in this case, the custom post type is a ContactInput, with a contact form on the actual site that I want to create a new post) to, how do I ensure I fill in the Metabox's Fields?

Basically, if the user submits a Form, I want to map those Form fields to a corresponding new Post with a custom post type and a set of Fields and save the new post. From the looks of it, I should be able to with wp_insert_post and update_post_meta, but I'm unsure what the meta key of the fields would be?

Cheers, Josh

jlambe commented 9 years ago

Your method is correct. When you specify a field name, the name is the meta key used to save the data in the database. So on the front-end, when calling the update_post_meta, simply specify the same key name of your field.

The framework do not alter key names so you're safe.

girvo commented 9 years ago

Beautiful! Now I just need to figure out why my Form is giving a "No route found" error :(

// routes.php

// Contact page
Route::get('page', ['contact', 'uses' => 'PagesController@getContact']);
Route::post('page', ['contact', 'uses' => 'PagesController@postContact']);
jlambe commented 9 years ago

Indeed that's weird. Can you show me the form code?

girvo commented 9 years ago
<div class="contact-form">
    @include('partials.formsuccess')
    @include('partials.formerrors')

    {{ Form::open() }}
    <div class="row">
        <div class="col-md-2">
            {{ Form::label('name', 'Your name: <i class="required">*</i>') }}
        </div>
        <div class="col-md-10">
            {{ Form::text('name', '', ['placeholder' => 'Your Name', 'required' => true]) }}
        </div>
    </div>

    <div class="row">
        <div class="col-md-2">
            {{ Form::label('email', 'Your email: <i class="required">*</i>') }}
        </div>
        <div class="col-md-10">
            {{ Form::email('email', '', ['placeholder' => 'you@example.com', 'required' => true]) }}
        </div>
    </div>

    <div class="row">
        <div class="col-md-2">
            {{ Form::label('phone', 'Your phone number:') }}
        </div>
        <div class="col-md-10">
            {{ Form::input('tel', 'phone', '', ['placeholder' => '0x xxxx xxxx']) }}
        </div>
    </div>

    <div class="row">
        <div class="col-md-2">
            {{ Form::label('message', 'Your query: <i class="required">*</i>') }}
        </div>
        <div class="col-md-10">
            {{ Form::textarea('message', '', ['placeholder' => 'Write your query here...', 'required' => true, 'rows' => 10]) }}
        </div>
    </div>

    <div class="row">
        <div class="col-xs-12 col-md-offset-2">
            {{ Form::submit('send', 'Send Enquiry') }}
        </div>
    </div>
    {{ Form::close() }}
</div>

The PagesController::postContact() method just does return "yep!"; currently, and it still gives a "404" error

jlambe commented 9 years ago

Have you turned on the permalinks?

girvo commented 9 years ago

I have, and I just reset it again just in case that was the problem. Bit of a weird one to be honest, I'm nearly 100% certain it was working previously!

girvo commented 9 years ago

Okay interesting, using httpie I can get it to hit the route, but it seems the form refuses to go through to it?

dbyd-themosis [master●] http POST http://dbyd.docker/contact
HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Length: 11
Content-Type: text/html; charset=UTF-8
Date: Mon, 07 Sep 2015 19:38:22 GMT
Keep-Alive: timeout=5, max=100
Link: <http://dbyd.docker/?p=10>; rel=shortlink
Server: Apache/2.4.7 (Ubuntu)
X-Pingback: http://dbyd.docker/cms/xmlrpc.php
X-Powered-By: PHP/5.5.9-1ubuntu4.11

Hello world

The form output is below:

                    <div class="contact-form">

    <form action="http://dbyd.docker/contact/" method="POST" accept-charset="UTF-8"><input type="hidden" id="_themosisnonce" name="_themosisnonce" value="bc6a3c88c2" /><input type="hidden" name="_wp_http_referer" value="/contact/" />    <div class="row">
        <div class="col-md-2">
            <label for="name">Your name: <i class="required">*</i></label>        </div>
        <div class="col-md-10">
            <input  placeholder="Your Name" required="1" type="text" name="name" value="">        </div>
    </div>

    <div class="row">
        <div class="col-md-2">
            <label for="email">Your email: <i class="required">*</i></label>        </div>
        <div class="col-md-10">
            <input  placeholder="you@example.com" required="1" type="email" name="email" value="">        </div>
    </div>

    <div class="row">
        <div class="col-md-2">
            <label for="phone">Your phone number:</label>        </div>
        <div class="col-md-10">
            <input  placeholder="0x xxxx xxxx" type="tel" name="phone" value="">        </div>
    </div>

    <div class="row">
        <div class="col-md-2">
            <label for="message">Your query: <i class="required">*</i></label>        </div>
        <div class="col-md-10">
            <textarea name="message"  placeholder="Write your query here..." required="1" rows="10" name="message"></textarea>        </div>
    </div>

    <div class="row">
        <div class="col-xs-12 col-md-offset-2">
            <button  type="submit" name="send">Send Enquiry</button>        </div>
    </div>
    </form></div>
girvo commented 9 years ago

And I've just ruled out something funky with the sessions, cleared my cache, everything. It seems that if it's coming from a browser it's refusing to work, which is weird as hell. I'll try removing my .htaccess and regenerating it just in case, and I'll let you know. Sorry for the spam -- trying to get this project out the door!

EDIT: Nope, regenerated the .htaccess and it's still exactly the same issue unfortunately.


# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

# END WordPress
jlambe commented 9 years ago

And what happens if you add the page ID in the route:

Route::post('page', [['contact', 10], 'uses' => 'PagesController@postContact']);

Does it catch it? Which version of the framework are you using? 1.1.2? or latest from master branch?

jlambe commented 9 years ago

Also does a Route::any() catches your requests?

girvo commented 9 years ago

Adding the page ID to the route didn't work, and using Route::any() gives the same error. Using Paw or HTTPie with ?p=10 instead of the page name gives a 404 as well

EDIT: Also, a POST request in Paw gives the correct result. It's just anything through the browser fails for some reason :/

jlambe commented 9 years ago

I just saw that you wrote PagesController::postContact() method returns 'yep'. In order to be sure, your postContact method should not be static:

public function postContact()
{
    return 'yep';
}
girvo commented 9 years ago

Yeah that was just a bad wording of it :) It's definitely not static!

So, I've worked around the issue thus far.

I have a custom /admin/api.php route condition, that links in a custom routing system that I wrote for Lean (a framework for Hack). I just enabled it, and sure enough I can POST from the form to it. It breaks the Wordpress loop however (it's meant for JSON API's) -- so I'll have to work around that for now.

// admin/api.php
<?php

/**
 * Custom routing system test
 */
global $matches;
function is_api_route(array $params) {
    global $matches;

    if (count($params) === 0 ||  !is_string($params[0])) {
        throw new Exception('Invalid route definition');
    }

    $route = buildRoute($params[0]);
    $path = $_SERVER['REQUEST_URI'];
    $results = testRoute($path, $route['regex'], $route['vars']);

    if ($results === null) {
        return false;
    }

    // Dirty global
    $matches = $results;

    return true;
}

function buildRoute($url) {
    if ($url === '/') {
        $result = array('regex' => '/^\/$/', 'vars' => []);
        return $result;
    }

    $split = explode('/', $url);
    $regex = '/^\/';
    $vars = [];

    // Now iterate over the exploded items, check if they're req vars, and set up the results
    foreach ($split as $item) {
        $item = (string) $item;

        if ($item === '') {
            continue;
        }

        $varName = isRequestVariable($item);

        if ($varName !== null) {
            $vars[] = $varName;
            $regex .= '([\w+-]+)';
        } else {
            $regex .= $item;
        }

        $regex .= '\/';
    }

    $regex .= '?$/';

    return array('regex' => $regex, 'vars' => $vars);
}

function isRequestVariable($item) {
    if (preg_match('/^\:\w+$/', $item)) {
        // remove colon for var name
        return substr($item, 1);
    }

    return null;
}

function testRoute($requestUri, $routeRegex, $routeVars) {
    $matches = [];
    $result = preg_match($routeRegex, $requestUri, $matches);
    if (count($matches) > 0) {
        $data = array_slice($matches, 1);
        $capture = array_combine($routeVars, $data);
        if ($result) {
            return $capture;
        }
    }

    return null;
}

add_filter('themosisRouteConditions', function() {
    $conds['api'] = 'is_api_route';
    return $conds;
});

Usage is as follows:

// routes.php

Route::get('page', ['contact', 'uses' => 'PagesController@getContact']);
Route::post('api', ['/contact', 'uses' => 'PagesController@postContact']);

That works well enough, but setup_postdata doesn't seem to fix the @loop even setting the global $post to be the right object:

    public function postContact() {
        // Dirty workaround for broken routing in Themosis
        global $post;
        $page = get_post(10);
        $post = $page;
        setup_postdata($post);
// ...
jlambe commented 9 years ago

You wrote the route is_api_route in order to catch the default contact page? not sure to follow along...

Anyway, in order to fix the main loop, its the main query you need to modify. The route system is using WordPress conditions to see which request to listen to. It doesn't check the URI as WordPress uses query parameters behind the scene.

If you want to alter the default main query, you need to hook to the pre_get_posts action. Also note the each route callback and controller method give you the main $post and $query instances.

So in your method:

public function postContact($post, $query)
{
    // $post is already the \WP_Post instance for the contact page
    setup_postdata($post); 
} 

Now you could simply run a new WP_Query and pass it to your view so you don't really care about the main loop.

girvo commented 9 years ago

I had already written the api post condition as a test (and had it turned off), I reenabled it to help try and debug what's going on with the Route::post problem. From the looks of it, for whatever reason the route condition of post('page') doesn't work when in a Form context -- I will see if I can replicate it in a fresh install for you.

Does the permalink setup need to be set to anything in particular?

jlambe commented 9 years ago

This is very strange as I just put online a new client website for forms and used the same routing to handle it. Is there any other Route::post calls in your routes.php?

The permalink is needed if you pass string parameters to the route condition. Keeping default, you'll need to use post/page IDs. So no, just any permalink option is fine.

Also, do you have another custom post type with a contact uri somewhere? Also, I just remembered you can't use some name attributes values for your form as these terms are reserved for WordPress:

I suspect your input with name=name. Try to pre-fix all your input and let me know how it goes?

jlambe commented 9 years ago

Is your issue resolved regarding POST request for your contact page?

girvo commented 9 years ago

Not yet, I've ruled out the contact slug being the issue by recreating it with a new slug (not editing it, deleting from the DB and recreating) -- I'm going to prefix the field names in an hour or so and will get back to you :)

Sent from my iPhone

On 11 Sep 2015, at 8:53 pm, Julien Lambé notifications@github.com wrote:

This is very strange as I just put online a new client website for forms and used the same routing to handle it. Is there any other Route::post calls in your routes.php?

The permalink is needed if you pass string parameters to the route condition. Keeping default, you'll need to use post/page IDs. So no, just any permalink option is fine.

Also, do you have another custom post type with a contact uri somewhere? Also, I just remembered you can't use some name attributes values for your form as these terms are reserved for WordPress:

https://codex.wordpress.org/Reserved_Terms I suspect your input with name=name. Try to pre-fix all your input and let me know how it goes?

— Reply to this email directly or view it on GitHub.

girvo commented 9 years ago

Okay, so.

I completely deleted the database, and set the entire site up from scratch again. The Form::open() call is exactly that, and my routes.php file is as follows;

<?php

/*
 * Define your routes and which views to display
 * depending of the query.
 *
 * Based on WordPress conditional tags from the WordPress Codex
 * http://codex.wordpress.org/Conditional_Tags
 *
 */

// Base pages
Route::get('front', 'PagesController@home');
Route::get('page', ['about', 'uses' => 'PagesController@about']);

// Contact page
Route::get('page', ['contact', 'uses' => 'PagesController@getContact']);
Route::post('page', ['contact', function($page) {
    return "Hello world!";
}]);

// Blog archive
Route::get('page', ['blog', 'uses' => 'BlogController@index']);

// Portfolio and project pages
Route::get('page', ['portfolio', 'uses' => 'PortfolioController@index']);
Route::get('singular', ['project', 'uses' => 'PortfolioController@project']);

I have once again stepped through the code -- and as soon as I press submit on the form, it gives me a routing error 404 once more. I have prefixed the contact-input's fields, and the portfolios fields. Nothing I do seems to fix it, and I'm at a loss here.

Crucially, if I POST to the page via HTTPie or Paw, it works, but with the Form it does not.

I'm going to send you an email with a link to the code if that's okay? I can't work out what's gone wrong, it seems like a bug but I've run out of places to look at in my own code.

Cheers, Josh

girvo commented 9 years ago

@jlambe

I am officially an idiot, however I think this is something that should probably be noted in the Form documentation.

For some reason I thought that prefixing the field names on my custom post types would fix it. It was the fact that I had a Form::text('name') field that caused the problem.

Sorry about that!

jlambe commented 9 years ago

@girvo Glad you resolved your issue. I will mention it in the new documentation but I think I'll add this #207 ticket to release 1.2.0. This will warn developers if they use a WordPress reserved term for inputs or custom fields ;-)