Working With View Composers in Sage 10

You can totally do it if you want! If the mental model works better for you then by all means. The best way to do a thing is the way that works best for you. I’ve used a similar pattern in situations where I’m calling partials in contexts where the state of my data is not assured and I just want to things to quietly go away instead of throwing “variable doesn’t exist errors,” but I’ve thought of it more as a sort of problem-recovery mechanic rather than an intentional architecture mechanic. YMMV though.

1 Like

If I dump $this->view I get a huge object. So I have to run a method on it. The below works, is this correct?

$this->view->name() === 'partials.entry-meta'

yeah as seen here

Of course! Thanks.

(20 characters)

@alwaysblank Wow, thank you so much for the thorough explanation!

1 Like

Hi guys,

I’m trying to create a base View Composer like described in this post, but somehow it’s not working for me. This is my base view composer:

namespace App\View\Composers;
use Roots\Acorn\View\Composer;

class ProductCardBase extends Composer {

	protected static $dataMethods = [
		'title',
		'description',
		'image',
		'product_link'
	];

	public function with() {
		$id = $this->data['product'];
		$product = wc_get_product($id);

		// Uncommenting this block does work?
		// return [
		// 	'title'        => $this->title($id, $product),
		// 	'description'  => $this->description($id, $product),
		// 	'image'        => $this->image($id, $product),
		// 	'page_link'    => $this->page_link($id, $product),
		// 	'product_link' => $this->product_link($id, $product)
		// ];

		return array_map(function($method) use ($id, $product) {
			if (method_exists($this, $method)) {
				return [$method => $this->{$method}($id, $product)];
			}

			return [$method => false];
		}, $this::$dataMethods);
	}

	/**
	 * Returns the product title.
	 *
	 * @return string
	 */
	public function title($id, $product) {
		return $product->get_name();
	}

	/**
	 * Returns the product short description.
	 *
	 * @return string
	 */
	public function description($id, $product) {
		return $product->get_short_description();
	}

	/**
	 * Returns the product image.
	 *
	 * @return string
	 */
	public function image($id, $product) {
		return $product->get_image('medium');
	}

	/**
	 * Returns the product url.
	 *
	 * @return string
	 */
	public function product_link($id, $product) {
		return $product->get_permalink();
	}
}

I extend it in a ProductBlock composer:

namespace App\View\Composers;

class ProductBlock extends ProductCardBase {
	/**
	 * List of views served by this composer.
	 *
	 * @var array
	 */
	protected static $views = [
		'blocks.product-block'
	];
}

In a loop, I include the partial and pass the product id:

@foreach ($products as $product)
	<div class="swiper-slide">
		@include('blocks.product-block', [
			'product' => $product
		])
	</div>
@endforeach

And my blocks.product-block partial:

@isset($product)
	<article class="b-product">
		<header class="b-product__header">
			<h3 class="b-product__title">{!! $title !!}</h3>
			<div class="b-product__desc">{!! $description !!}</div>
		</header>
		@if ($image)
			<figure class="b-product__image">{!! $image !!}</figure>
		@endif
		<footer class="b-product__footer">
			@if ($product_link)
				<a class="btn btn-outline-primary" href="{{ $product_link }}" role="button">{!! __('Configure', 'mydomain') !!}</a>
			@endif
		</footer>
	</article>
@endisset

Strangely, if I just return the commented array with the function calls directly, it does work?
I thought it had to do with how the class functions are called by string name:

$this->{$method}($id, $product)

so instead I tried:

call_user_func([$this, $method], $id, $product);

with same result, not data available in my view?
What am I missing here? Is this array_map technique not possible anymore with the latest Composer?

Thanks!

with() needs to return a keyed array: those keys are used as the variable names in your blade. Your usage of array_map() doesn’t return keys, it returns an array of arrays. array_map() in PHP can’t modify or generate keys, it only handles values. You appear to be trying to generate keys through it, which won’t work.

Thanks, you’re right! I just figured that out too…
I literally copied it form the original article:

public function with($data, $view)
    {
        return array_map(function($method) use ($data, $view) {
            if (method_exists($this, $method)) {
                return [$method => $this->{$method}($data, $view)];
            }

            return [$method => false];
        }, $this::$dataMethods);
    }

I think I need to use array_walk instead?

A simple foreach loop works:

$methods = [];
foreach($this::$dataMethods as $key => $method) {
	if (method_exists($this, $method)) {
		$methods[$method] = $this->{$method}($id, $product);
	} else {
		$methods[$method] = false;
	}
}

return $methods;

array_walk() still can’t modify keys, I think. You could probably use array_column():

return array_column(array_map(function($method) use ($data, $view) {
            if (method_exists($this, $method)) {
                return [$method, $this->{$method}($data, $view)];
            }

            return [$method, false];
        }, $this::$dataMethods), 1, 0);

Anyway, thanks for noticing that bug! I’m surprised you’re the first person to try and use that code and realize it’s broken. I’ll fix it in the post.

2 Likes

I was surprised too :slight_smile:
Is it not a new PHP 7.4 restriction?

EDIT: no it’s not

1 Like

I still feel like I’m missing something probably pretty obvious when it comes to this, but how do I pass params from my views to the Composer? From what I’ve read and tried to troubleshoot on my own, it doesn’t seem that Composers allow for static functions?

For example, in Sage 9 I could do something like {!! App::getSomePosts('testimonials', 3) !!} and then in my Controller have

public static function getSomePosts($type, $num) {
        $args = [
            'post_type' => $type,
            'posts_per_page' => $num,
        ];
        $query = new \WP_Query($args);

        return $query;
    }

Then I could loop over the result in my view. How can I create these sort of helper functions with Composers? It seems like passing params isn’t an option?

Make it available using with or override, I believe.

In a view composer:

class MyView {
  protected static $views = [
      'partials.content',
  ];

  public function with()
  {
      return [
          'posts' => $this->posts(),       
      ];
  }

  public function posts()
  {
        $args = [
            'post_type' => $type,
            'posts_per_page' => $num,
        ];
        $query = new \WP_Query($args);

        return $query;
  }
}

In a view:

  <div class="entry-content">
    <!-- do something with the posts -->
    {!! $posts !!}
  </div>
2 Likes

But if the post type and number are unknown until I need to use it in a view, how do I pass the type and num params? I want this function be available in all views, so for example I could use the same function and pass “post” and “10” instead of “testimonials” and “3” in a completely different view.

If this is your use case, then a Composer isn’t really the place for your function. Composers are for modifying and providing data to views, they’re not meant to be wrappers for methods. I’d put your function somewhere else (i.e. helpers.php) and then just call it from your blades like get_my_posts('post', 13).

If you’re absolutely determined to put this functionality in your view, though, then you just need to return a function:

// Composer
public function with()
{
  return [
    'getPosts' => function($post_type, $num) {
      return new \WP_Query([
        'post_type' => $post_type,
        'posts_per_page' => $num,
      ]),
 ];
}
// blade
@foreach( $getPosts('post', 10) as $post )
  // Do Stuff
@endforeach
3 Likes

Okay, that makes much more sense. Thank you for your help and all the work you do to make Sage great!

1 Like

I would like to display a custom post type by categories so each category can be displayed under different tabs. So far this is the best example I have seen. I have added a query similar to your example.

  protected static $views = [
        'partials.page-header',
        'partials.hero',
        'partials.content',
        'partials.content-*',
    ];

public function override()
    {
        return [
            'title' => $this->title(),
            'projects' => $this->projects(),
        ];
    }

    public function projects(): WP_Query
    {
        $args = [
            'post_type' => 'projects',
            'category_name' => 'email',
            'posts_per_page' => 10,
        ];
        return new \WP_Query($args);
    }

In a view

<article @php(post_class())>
  <header>
    <h2 class="entry-title">
      <a href="{{ get_permalink() }}">
        {!! $title !!}
      </a>
    </h2>

    @include('partials.entry-meta')
  </header>
  {!! $projects !!}
  <div class="entry-summary">
    @php(the_excerpt())
  </div>
</article>

The error I get returned is:

Any help or insight would be great. Thank you.

Have you read the error? The first line tells you exactly what’s wrong.

I’m sure I’m missing something and not understanding what the error is. I was able to use your previous last solution, and get it to render, but unclear on getting to just show that category correctly in an archive view. I am getting duplicates or anything other than expected results.

I updated, but other categories continue display.

  public function with()
    {
        return [
            'getPosts' => function ($post_type, $num, $category) {
                return new \WP_Query([
                    'post_type' => $post_type,
                    'posts_per_page' => $num,
                    'category_name' => $category,
                ]);
            }
        ];
    }

Thank you.

You’re not providing enough context and code here for us to effectively debug your issue.

  • The error you posted says you’re trying to echo a WP_Query object, which you can’t do. You need to modify your code to loop over the query results.
  • I don’t understand what you’re asking with your most recent post: you’ve only posted a small snippet of code, but no indication of how it’s used.