protected function _filterSubject($subject, $frame, $flags) {
    // check @type (object value means 'any' type, fall through to ducktyping)
    if(property_exists($frame, '@type') &&
      !(count($frame->{'@type'}) === 1 && is_object($frame->{'@type'}[0]))) {
      $types = $frame->{'@type'};
      foreach($types as $type) {
        // any matching @type is a match
        if(self::hasValue($subject, '@type', $type)) {
          return true;
        }
      }
      return false;
    }

    // check ducktype
    $wildcard = true;
    $matches_some = false;
    foreach($frame as $k => $v) {
      if(self::_isKeyword($k)) {
        // skip non-@id and non-@type
        if($k !== '@id' && $k !== '@type') {
          continue;
        }
        $wildcard = false;

        // check @id for a specific @id value
        if($k === '@id' && is_string($v)) {
          if(!property_exists($subject, $k) || $subject->{$k} !== $v) {
            return false;
          }
          $matches_some = true;
          continue;
        }
      }

      $wildcard = false;

      if(property_exists($subject, $k)) {
        // $v === [] means do not match if property is present
        if(is_array($v) && count($v) === 0) {
          return false;
        }
        $matches_some = true;
        continue;
      }

      // all properties must match to be a duck unless a @default is specified
      $has_default = (is_array($v) && count($v) === 1 && is_object($v[0]) &&
        property_exists($v[0], '@default'));
      if($flags['requireAll'] && !$has_default) {
        return false;
      }
    }

    // return true if wildcard or subject matches some properties
    return $wildcard || $matches_some;
  }